Move third_party code under third_party/src so it can be used in $GOPATH.

This commit is contained in:
Joe Beda
2014-06-13 15:15:38 -07:00
parent a40529b379
commit d230625e1a
127 changed files with 7 additions and 7 deletions

View File

@@ -0,0 +1,20 @@
Copyright (c) 2012 Daniel Theophanes
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.

View File

@@ -0,0 +1,32 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Extensions to the standard "os" package.
package osext
import "path/filepath"
// Executable returns an absolute path that can be used to
// re-invoke the current program.
// It may not be valid after the current program exits.
func Executable() (string, error) {
p, err := executable()
return filepath.Clean(p), err
}
// Returns same path as Executable, returns just the folder
// path. Excludes the executable name.
func ExecutableFolder() (string, error) {
p, err := Executable()
if err != nil {
return "", err
}
folder, _ := filepath.Split(p)
return folder, nil
}
// Depricated. Same as Executable().
func GetExePath() (exePath string, err error) {
return Executable()
}

View File

@@ -0,0 +1,20 @@
// Copyright 2012 The Go 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 osext
import (
"syscall"
"os"
"strconv"
)
func executable() (string, error) {
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
if err != nil {
return "", err
}
defer f.Close()
return syscall.Fd2path(int(f.Fd()))
}

View File

@@ -0,0 +1,25 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux netbsd openbsd
package osext
import (
"errors"
"os"
"runtime"
)
func executable() (string, error) {
switch runtime.GOOS {
case "linux":
return os.Readlink("/proc/self/exe")
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "openbsd":
return os.Readlink("/proc/curproc/file")
}
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
}

View File

@@ -0,0 +1,82 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin freebsd
package osext
import (
"os"
"path/filepath"
"runtime"
"syscall"
"unsafe"
)
var startUpcwd, getwdError = os.Getwd()
func executable() (string, error) {
var mib [4]int32
switch runtime.GOOS {
case "freebsd":
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
case "darwin":
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
}
n := uintptr(0)
// get length
_, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
if err != 0 {
return "", err
}
if n == 0 { // shouldn't happen
return "", nil
}
buf := make([]byte, n)
_, _, err = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
if err != 0 {
return "", err
}
if n == 0 { // shouldn't happen
return "", nil
}
for i, v := range buf {
if v == 0 {
buf = buf[:i]
break
}
}
var strpath string
if buf[0] != '/' {
var e error
if strpath, e = getAbs(buf); e != nil {
return strpath, e
}
} else {
strpath = string(buf)
}
// darwin KERN_PROCARGS may return the path to a symlink rather than the
// actual executable
if runtime.GOOS == "darwin" {
if strpath, err := filepath.EvalSymlinks(strpath); err != nil {
return strpath, err
}
}
return strpath, nil
}
func getAbs(buf []byte) (string, error) {
if getwdError != nil {
return string(buf), getwdError
} else {
if buf[0] == '.' {
buf = buf[1:]
}
if startUpcwd[len(startUpcwd)-1] != '/' && buf[0] != '/' {
return startUpcwd + "/" + string(buf), nil
}
return startUpcwd + string(buf), nil
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin linux freebsd netbsd windows
package osext
import (
"fmt"
"os"
oexec "os/exec"
"path/filepath"
"runtime"
"testing"
)
const execPath_EnvVar = "OSTEST_OUTPUT_EXECPATH"
func TestExecPath(t *testing.T) {
ep, err := Executable()
if err != nil {
t.Fatalf("ExecPath failed: %v", err)
}
// we want fn to be of the form "dir/prog"
dir := filepath.Dir(filepath.Dir(ep))
fn, err := filepath.Rel(dir, ep)
if err != nil {
t.Fatalf("filepath.Rel: %v", err)
}
cmd := &oexec.Cmd{}
// make child start with a relative program path
cmd.Dir = dir
cmd.Path = fn
// forge argv[0] for child, so that we can verify we could correctly
// get real path of the executable without influenced by argv[0].
cmd.Args = []string{"-", "-test.run=XXXX"}
cmd.Env = []string{fmt.Sprintf("%s=1", execPath_EnvVar)}
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("exec(self) failed: %v", err)
}
outs := string(out)
if !filepath.IsAbs(outs) {
t.Fatalf("Child returned %q, want an absolute path", out)
}
if !sameFile(outs, ep) {
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
}
}
func sameFile(fn1, fn2 string) bool {
fi1, err := os.Stat(fn1)
if err != nil {
return false
}
fi2, err := os.Stat(fn2)
if err != nil {
return false
}
return os.SameFile(fi1, fi2)
}
func init() {
if e := os.Getenv(execPath_EnvVar); e != "" {
// first chdir to another path
dir := "/"
if runtime.GOOS == "windows" {
dir = filepath.VolumeName(".")
}
os.Chdir(dir)
if ep, err := Executable(); err != nil {
fmt.Fprint(os.Stderr, "ERROR: ", err)
} else {
fmt.Fprint(os.Stderr, ep)
}
os.Exit(0)
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2012 The Go 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 osext
import (
"syscall"
"unicode/utf16"
"unsafe"
)
var (
kernel = syscall.MustLoadDLL("kernel32.dll")
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
)
// GetModuleFileName() with hModule = NULL
func executable() (exePath string, err error) {
return getModuleFileName()
}
func getModuleFileName() (string, error) {
var n uint32
b := make([]uint16, syscall.MAX_PATH)
size := uint32(len(b))
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
n = uint32(r0)
if n == 0 {
return "", e1
}
return string(utf16.Decode(b[0:n])), nil
}

View File

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

View File

@@ -0,0 +1,15 @@
# go-etcd
The official etcd v0.2 client library for Go.
For usage, please refer to [![GoDoc](https://godoc.org/github.com/coreos/go-etcd/etcd?status.png)](https://godoc.org/github.com/coreos/go-etcd/etcd)
## Install
```bash
go get github.com/coreos/go-etcd/etcd
```
## License
See LICENSE file.

View File

@@ -0,0 +1,23 @@
package etcd
// Add a new directory with a random etcd-generated key under the given path.
func (c *Client) AddChildDir(key string, ttl uint64) (*Response, error) {
raw, err := c.post(key, "", ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// Add a new file with a random etcd-generated key under the given path.
func (c *Client) AddChild(key string, value string, ttl uint64) (*Response, error) {
raw, err := c.post(key, value, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}

View File

@@ -0,0 +1,73 @@
package etcd
import "testing"
func TestAddChild(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("fooDir", true)
c.Delete("nonexistentDir", true)
}()
c.CreateDir("fooDir", 5)
_, err := c.AddChild("fooDir", "v0", 5)
if err != nil {
t.Fatal(err)
}
_, err = c.AddChild("fooDir", "v1", 5)
if err != nil {
t.Fatal(err)
}
resp, err := c.Get("fooDir", true, false)
// The child with v0 should proceed the child with v1 because it's added
// earlier, so it should have a lower key.
if !(len(resp.Node.Nodes) == 2 && (resp.Node.Nodes[0].Value == "v0" && resp.Node.Nodes[1].Value == "v1")) {
t.Fatalf("AddChild 1 failed. There should be two chlidren whose values are v0 and v1, respectively."+
" The response was: %#v", resp)
}
// Creating a child under a nonexistent directory should succeed.
// The directory should be created.
resp, err = c.AddChild("nonexistentDir", "foo", 5)
if err != nil {
t.Fatal(err)
}
}
func TestAddChildDir(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("fooDir", true)
c.Delete("nonexistentDir", true)
}()
c.CreateDir("fooDir", 5)
_, err := c.AddChildDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
_, err = c.AddChildDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
resp, err := c.Get("fooDir", true, false)
// The child with v0 should proceed the child with v1 because it's added
// earlier, so it should have a lower key.
if !(len(resp.Node.Nodes) == 2 && (len(resp.Node.Nodes[0].Nodes) == 0 && len(resp.Node.Nodes[1].Nodes) == 0)) {
t.Fatalf("AddChildDir 1 failed. There should be two chlidren whose values are v0 and v1, respectively."+
" The response was: %#v", resp)
}
// Creating a child under a nonexistent directory should succeed.
// The directory should be created.
resp, err = c.AddChildDir("nonexistentDir", 5)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,430 @@
package etcd
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"time"
)
// See SetConsistency for how to use these constants.
const (
// Using strings rather than iota because the consistency level
// could be persisted to disk, so it'd be better to use
// human-readable values.
STRONG_CONSISTENCY = "STRONG"
WEAK_CONSISTENCY = "WEAK"
)
const (
defaultBufferSize = 10
)
type Config struct {
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
CaCertFile []string `json:"caCertFiles"`
DialTimeout time.Duration `json:"timeout"`
Consistency string `json:"consistency"`
}
type Client struct {
config Config `json:"config"`
cluster *Cluster `json:"cluster"`
httpClient *http.Client
persistence io.Writer
cURLch chan string
// CheckRetry can be used to control the policy for failed requests
// and modify the cluster if needed.
// The client calls it before sending requests again, and
// stops retrying if CheckRetry returns some error. The cases that
// this function needs to handle include no response and unexpected
// http status code of response.
// If CheckRetry is nil, client will call the default one
// `DefaultCheckRetry`.
// Argument cluster is the etcd.Cluster object that these requests have been made on.
// Argument reqs is all of the http.Requests that have been made so far.
// Argument resps is all of the http.Responses from these requests.
// Argument err is the reason of the failure.
CheckRetry func(cluster *Cluster, reqs []http.Request,
resps []http.Response, err error) error
}
// NewClient create a basic client that is configured to be used
// with the given machine list.
func NewClient(machines []string) *Client {
config := Config{
// default timeout is one second
DialTimeout: time.Second,
// default consistency level is STRONG
Consistency: STRONG_CONSISTENCY,
}
client := &Client{
cluster: NewCluster(machines),
config: config,
}
client.initHTTPClient()
client.saveConfig()
return client
}
// NewTLSClient create a basic client with TLS configuration
func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error) {
// overwrite the default machine to use https
if len(machines) == 0 {
machines = []string{"https://127.0.0.1:4001"}
}
config := Config{
// default timeout is one second
DialTimeout: time.Second,
// default consistency level is STRONG
Consistency: STRONG_CONSISTENCY,
CertFile: cert,
KeyFile: key,
CaCertFile: make([]string, 0),
}
client := &Client{
cluster: NewCluster(machines),
config: config,
}
err := client.initHTTPSClient(cert, key)
if err != nil {
return nil, err
}
err = client.AddRootCA(caCert)
client.saveConfig()
return client, nil
}
// NewClientFromFile creates a client from a given file path.
// The given file is expected to use the JSON format.
func NewClientFromFile(fpath string) (*Client, error) {
fi, err := os.Open(fpath)
if err != nil {
return nil, err
}
defer func() {
if err := fi.Close(); err != nil {
panic(err)
}
}()
return NewClientFromReader(fi)
}
// NewClientFromReader creates a Client configured from a given reader.
// The configuration is expected to use the JSON format.
func NewClientFromReader(reader io.Reader) (*Client, error) {
c := new(Client)
b, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, c)
if err != nil {
return nil, err
}
if c.config.CertFile == "" {
c.initHTTPClient()
} else {
err = c.initHTTPSClient(c.config.CertFile, c.config.KeyFile)
}
if err != nil {
return nil, err
}
for _, caCert := range c.config.CaCertFile {
if err := c.AddRootCA(caCert); err != nil {
return nil, err
}
}
return c, nil
}
// Override the Client's HTTP Transport object
func (c *Client) SetTransport(tr *http.Transport) {
c.httpClient.Transport = tr
}
// initHTTPClient initializes a HTTP client for etcd client
func (c *Client) initHTTPClient() {
tr := &http.Transport{
Dial: c.dial,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
c.httpClient = &http.Client{Transport: tr}
}
// initHTTPClient initializes a HTTPS client for etcd client
func (c *Client) initHTTPSClient(cert, key string) error {
if cert == "" || key == "" {
return errors.New("Require both cert and key path")
}
tlsCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return err
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
InsecureSkipVerify: true,
}
tr := &http.Transport{
TLSClientConfig: tlsConfig,
Dial: c.dial,
}
c.httpClient = &http.Client{Transport: tr}
return nil
}
// SetPersistence sets a writer to which the config will be
// written every time it's changed.
func (c *Client) SetPersistence(writer io.Writer) {
c.persistence = writer
}
// SetConsistency changes the consistency level of the client.
//
// When consistency is set to STRONG_CONSISTENCY, all requests,
// including GET, are sent to the leader. This means that, assuming
// the absence of leader failures, GET requests are guaranteed to see
// the changes made by previous requests.
//
// When consistency is set to WEAK_CONSISTENCY, other requests
// are still sent to the leader, but GET requests are sent to a
// random server from the server pool. This reduces the read
// load on the leader, but it's not guaranteed that the GET requests
// will see changes made by previous requests (they might have not
// yet been committed on non-leader servers).
func (c *Client) SetConsistency(consistency string) error {
if !(consistency == STRONG_CONSISTENCY || consistency == WEAK_CONSISTENCY) {
return errors.New("The argument must be either STRONG_CONSISTENCY or WEAK_CONSISTENCY.")
}
c.config.Consistency = consistency
return nil
}
// AddRootCA adds a root CA cert for the etcd client
func (c *Client) AddRootCA(caCert string) error {
if c.httpClient == nil {
return errors.New("Client has not been initialized yet!")
}
certBytes, err := ioutil.ReadFile(caCert)
if err != nil {
return err
}
tr, ok := c.httpClient.Transport.(*http.Transport)
if !ok {
panic("AddRootCA(): Transport type assert should not fail")
}
if tr.TLSClientConfig.RootCAs == nil {
caCertPool := x509.NewCertPool()
ok = caCertPool.AppendCertsFromPEM(certBytes)
if ok {
tr.TLSClientConfig.RootCAs = caCertPool
}
tr.TLSClientConfig.InsecureSkipVerify = false
} else {
ok = tr.TLSClientConfig.RootCAs.AppendCertsFromPEM(certBytes)
}
if !ok {
err = errors.New("Unable to load caCert")
}
c.config.CaCertFile = append(c.config.CaCertFile, caCert)
c.saveConfig()
return err
}
// SetCluster updates cluster information using the given machine list.
func (c *Client) SetCluster(machines []string) bool {
success := c.internalSyncCluster(machines)
return success
}
func (c *Client) GetCluster() []string {
return c.cluster.Machines
}
// SyncCluster updates the cluster information using the internal machine list.
func (c *Client) SyncCluster() bool {
return c.internalSyncCluster(c.cluster.Machines)
}
// internalSyncCluster syncs cluster information using the given machine list.
func (c *Client) internalSyncCluster(machines []string) bool {
for _, machine := range machines {
httpPath := c.createHttpPath(machine, path.Join(version, "machines"))
resp, err := c.httpClient.Get(httpPath)
if err != nil {
// try another machine in the cluster
continue
} else {
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
// try another machine in the cluster
continue
}
// update Machines List
c.cluster.updateFromStr(string(b))
// update leader
// the first one in the machine list is the leader
c.cluster.switchLeader(0)
logger.Debug("sync.machines ", c.cluster.Machines)
c.saveConfig()
return true
}
}
return false
}
// createHttpPath creates a complete HTTP URL.
// serverName should contain both the host name and a port number, if any.
func (c *Client) createHttpPath(serverName string, _path string) string {
u, err := url.Parse(serverName)
if err != nil {
panic(err)
}
u.Path = path.Join(u.Path, _path)
if u.Scheme == "" {
u.Scheme = "http"
}
return u.String()
}
// dial attempts to open a TCP connection to the provided address, explicitly
// enabling keep-alives with a one-second interval.
func (c *Client) dial(network, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(network, addr, c.config.DialTimeout)
if err != nil {
return nil, err
}
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("Failed type-assertion of net.Conn as *net.TCPConn")
}
// Keep TCP alive to check whether or not the remote machine is down
if err = tcpConn.SetKeepAlive(true); err != nil {
return nil, err
}
if err = tcpConn.SetKeepAlivePeriod(time.Second); err != nil {
return nil, err
}
return tcpConn, nil
}
func (c *Client) OpenCURL() {
c.cURLch = make(chan string, defaultBufferSize)
}
func (c *Client) CloseCURL() {
c.cURLch = nil
}
func (c *Client) sendCURL(command string) {
go func() {
select {
case c.cURLch <- command:
default:
}
}()
}
func (c *Client) RecvCURL() string {
return <-c.cURLch
}
// saveConfig saves the current config using c.persistence.
func (c *Client) saveConfig() error {
if c.persistence != nil {
b, err := json.Marshal(c)
if err != nil {
return err
}
_, err = c.persistence.Write(b)
if err != nil {
return err
}
}
return nil
}
// MarshalJSON implements the Marshaller interface
// as defined by the standard JSON package.
func (c *Client) MarshalJSON() ([]byte, error) {
b, err := json.Marshal(struct {
Config Config `json:"config"`
Cluster *Cluster `json:"cluster"`
}{
Config: c.config,
Cluster: c.cluster,
})
if err != nil {
return nil, err
}
return b, nil
}
// UnmarshalJSON implements the Unmarshaller interface
// as defined by the standard JSON package.
func (c *Client) UnmarshalJSON(b []byte) error {
temp := struct {
Config Config `json:"config"`
Cluster *Cluster `json:"cluster"`
}{}
err := json.Unmarshal(b, &temp)
if err != nil {
return err
}
c.cluster = temp.Cluster
c.config = temp.Config
return nil
}

View File

@@ -0,0 +1,96 @@
package etcd
import (
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"testing"
)
// To pass this test, we need to create a cluster of 3 machines
// The server should be listening on 127.0.0.1:4001, 4002, 4003
func TestSync(t *testing.T) {
fmt.Println("Make sure there are three nodes at 0.0.0.0:4001-4003")
// Explicit trailing slash to ensure this doesn't reproduce:
// https://github.com/coreos/go-etcd/issues/82
c := NewClient([]string{"http://127.0.0.1:4001/"})
success := c.SyncCluster()
if !success {
t.Fatal("cannot sync machines")
}
for _, m := range c.GetCluster() {
u, err := url.Parse(m)
if err != nil {
t.Fatal(err)
}
if u.Scheme != "http" {
t.Fatal("scheme must be http")
}
host, _, err := net.SplitHostPort(u.Host)
if err != nil {
t.Fatal(err)
}
if host != "127.0.0.1" {
t.Fatal("Host must be 127.0.0.1")
}
}
badMachines := []string{"abc", "edef"}
success = c.SetCluster(badMachines)
if success {
t.Fatal("should not sync on bad machines")
}
goodMachines := []string{"127.0.0.1:4002"}
success = c.SetCluster(goodMachines)
if !success {
t.Fatal("cannot sync machines")
} else {
fmt.Println(c.cluster.Machines)
}
}
func TestPersistence(t *testing.T) {
c := NewClient(nil)
c.SyncCluster()
fo, err := os.Create("config.json")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
c.SetPersistence(fo)
err = c.saveConfig()
if err != nil {
t.Fatal(err)
}
c2, err := NewClientFromFile("config.json")
if err != nil {
t.Fatal(err)
}
// Verify that the two clients have the same config
b1, _ := json.Marshal(c)
b2, _ := json.Marshal(c2)
if string(b1) != string(b2) {
t.Fatalf("The two configs should be equal!")
}
}

View File

@@ -0,0 +1,51 @@
package etcd
import (
"net/url"
"strings"
)
type Cluster struct {
Leader string `json:"leader"`
Machines []string `json:"machines"`
}
func NewCluster(machines []string) *Cluster {
// if an empty slice was sent in then just assume HTTP 4001 on localhost
if len(machines) == 0 {
machines = []string{"http://127.0.0.1:4001"}
}
// default leader and machines
return &Cluster{
Leader: machines[0],
Machines: machines,
}
}
// switchLeader switch the current leader to machines[num]
func (cl *Cluster) switchLeader(num int) {
logger.Debugf("switch.leader[from %v to %v]",
cl.Leader, cl.Machines[num])
cl.Leader = cl.Machines[num]
}
func (cl *Cluster) updateFromStr(machines string) {
cl.Machines = strings.Split(machines, ", ")
}
func (cl *Cluster) updateLeader(leader string) {
logger.Debugf("update.leader[%s,%s]", cl.Leader, leader)
cl.Leader = leader
}
func (cl *Cluster) updateLeaderFromURL(u *url.URL) {
var leader string
if u.Scheme == "" {
leader = "http://" + u.Host
} else {
leader = u.Scheme + "://" + u.Host
}
cl.updateLeader(leader)
}

View File

@@ -0,0 +1,34 @@
package etcd
import "fmt"
func (c *Client) CompareAndDelete(key string, prevValue string, prevIndex uint64) (*Response, error) {
raw, err := c.RawCompareAndDelete(key, prevValue, prevIndex)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uint64) (*RawResponse, error) {
if prevValue == "" && prevIndex == 0 {
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
}
options := Options{}
if prevValue != "" {
options["prevValue"] = prevValue
}
if prevIndex != 0 {
options["prevIndex"] = prevIndex
}
raw, err := c.delete(key, options)
if err != nil {
return nil, err
}
return raw, err
}

View File

@@ -0,0 +1,46 @@
package etcd
import (
"testing"
)
func TestCompareAndDelete(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
}()
c.Set("foo", "bar", 5)
// This should succeed an correct prevValue
resp, err := c.CompareAndDelete("foo", "bar", 0)
if err != nil {
t.Fatal(err)
}
if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
t.Fatalf("CompareAndDelete 1 prevNode failed: %#v", resp)
}
resp, _ = c.Set("foo", "bar", 5)
// This should fail because it gives an incorrect prevValue
_, err = c.CompareAndDelete("foo", "xxx", 0)
if err == nil {
t.Fatalf("CompareAndDelete 2 should have failed. The response is: %#v", resp)
}
// This should succeed because it gives an correct prevIndex
resp, err = c.CompareAndDelete("foo", "", resp.Node.ModifiedIndex)
if err != nil {
t.Fatal(err)
}
if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp)
}
c.Set("foo", "bar", 5)
// This should fail because it gives an incorrect prevIndex
resp, err = c.CompareAndDelete("foo", "", 29817514)
if err == nil {
t.Fatalf("CompareAndDelete 4 should have failed. The response is: %#v", resp)
}
}

View File

@@ -0,0 +1,36 @@
package etcd
import "fmt"
func (c *Client) CompareAndSwap(key string, value string, ttl uint64,
prevValue string, prevIndex uint64) (*Response, error) {
raw, err := c.RawCompareAndSwap(key, value, ttl, prevValue, prevIndex)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64,
prevValue string, prevIndex uint64) (*RawResponse, error) {
if prevValue == "" && prevIndex == 0 {
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
}
options := Options{}
if prevValue != "" {
options["prevValue"] = prevValue
}
if prevIndex != 0 {
options["prevIndex"] = prevIndex
}
raw, err := c.put(key, value, ttl, options)
if err != nil {
return nil, err
}
return raw, err
}

View File

@@ -0,0 +1,57 @@
package etcd
import (
"testing"
)
func TestCompareAndSwap(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
}()
c.Set("foo", "bar", 5)
// This should succeed
resp, err := c.CompareAndSwap("foo", "bar2", 5, "bar", 0)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
t.Fatalf("CompareAndSwap 1 failed: %#v", resp)
}
if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
t.Fatalf("CompareAndSwap 1 prevNode failed: %#v", resp)
}
// This should fail because it gives an incorrect prevValue
resp, err = c.CompareAndSwap("foo", "bar3", 5, "xxx", 0)
if err == nil {
t.Fatalf("CompareAndSwap 2 should have failed. The response is: %#v", resp)
}
resp, err = c.Set("foo", "bar", 5)
if err != nil {
t.Fatal(err)
}
// This should succeed
resp, err = c.CompareAndSwap("foo", "bar2", 5, "", resp.Node.ModifiedIndex)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
t.Fatalf("CompareAndSwap 3 failed: %#v", resp)
}
if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp)
}
// This should fail because it gives an incorrect prevIndex
resp, err = c.CompareAndSwap("foo", "bar3", 5, "", 29817514)
if err == nil {
t.Fatalf("CompareAndSwap 4 should have failed. The response is: %#v", resp)
}
}

View File

@@ -0,0 +1,54 @@
package etcd
import (
"io/ioutil"
"log"
"strings"
)
var logger *etcdLogger
func SetLogger(l *log.Logger) {
logger = &etcdLogger{l}
}
func GetLogger() *log.Logger {
return logger.log
}
type etcdLogger struct {
log *log.Logger
}
func (p *etcdLogger) Debug(args ...interface{}) {
args[0] = "DEBUG: " + args[0].(string)
p.log.Println(args)
}
func (p *etcdLogger) Debugf(fmt string, args ...interface{}) {
args[0] = "DEBUG: " + args[0].(string)
// Append newline if necessary
if !strings.HasSuffix(fmt, "\n") {
fmt = fmt + "\n"
}
p.log.Printf(fmt, args)
}
func (p *etcdLogger) Warning(args ...interface{}) {
args[0] = "WARNING: " + args[0].(string)
p.log.Println(args)
}
func (p *etcdLogger) Warningf(fmt string, args ...interface{}) {
// Append newline if necessary
if !strings.HasSuffix(fmt, "\n") {
fmt = fmt + "\n"
}
args[0] = "WARNING: " + args[0].(string)
p.log.Printf(fmt, args)
}
func init() {
// Default logger uses the go default log.
SetLogger(log.New(ioutil.Discard, "go-etcd", log.LstdFlags))
}

View File

@@ -0,0 +1,40 @@
package etcd
// Delete deletes the given key.
//
// When recursive set to false, if the key points to a
// directory the method will fail.
//
// When recursive set to true, if the key points to a file,
// the file will be deleted; if the key points to a directory,
// then everything under the directory (including all child directories)
// will be deleted.
func (c *Client) Delete(key string, recursive bool) (*Response, error) {
raw, err := c.RawDelete(key, recursive, false)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// DeleteDir deletes an empty directory or a key value pair
func (c *Client) DeleteDir(key string) (*Response, error) {
raw, err := c.RawDelete(key, false, true)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) {
ops := Options{
"recursive": recursive,
"dir": dir,
}
return c.delete(key, ops)
}

View File

@@ -0,0 +1,81 @@
package etcd
import (
"testing"
)
func TestDelete(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
}()
c.Set("foo", "bar", 5)
resp, err := c.Delete("foo", false)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Value == "") {
t.Fatalf("Delete failed with %s", resp.Node.Value)
}
if !(resp.PrevNode.Value == "bar") {
t.Fatalf("Delete PrevNode failed with %s", resp.Node.Value)
}
resp, err = c.Delete("foo", false)
if err == nil {
t.Fatalf("Delete should have failed because the key foo did not exist. "+
"The response was: %v", resp)
}
}
func TestDeleteAll(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
c.Delete("fooDir", true)
}()
c.SetDir("foo", 5)
// test delete an empty dir
resp, err := c.DeleteDir("foo")
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Value == "") {
t.Fatalf("DeleteAll 1 failed: %#v", resp)
}
if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") {
t.Fatalf("DeleteAll 1 PrevNode failed: %#v", resp)
}
c.CreateDir("fooDir", 5)
c.Set("fooDir/foo", "bar", 5)
_, err = c.DeleteDir("fooDir")
if err == nil {
t.Fatal("should not able to delete a non-empty dir with deletedir")
}
resp, err = c.Delete("fooDir", true)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Value == "") {
t.Fatalf("DeleteAll 2 failed: %#v", resp)
}
if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") {
t.Fatalf("DeleteAll 2 PrevNode failed: %#v", resp)
}
resp, err = c.Delete("foo", true)
if err == nil {
t.Fatalf("DeleteAll should have failed because the key foo did not exist. "+
"The response was: %v", resp)
}
}

View File

@@ -0,0 +1,48 @@
package etcd
import (
"encoding/json"
"fmt"
)
const (
ErrCodeEtcdNotReachable = 501
)
var (
errorMap = map[int]string{
ErrCodeEtcdNotReachable: "All the given peers are not reachable",
}
)
type EtcdError struct {
ErrorCode int `json:"errorCode"`
Message string `json:"message"`
Cause string `json:"cause,omitempty"`
Index uint64 `json:"index"`
}
func (e EtcdError) Error() string {
return fmt.Sprintf("%v: %v (%v) [%v]", e.ErrorCode, e.Message, e.Cause, e.Index)
}
func newError(errorCode int, cause string, index uint64) *EtcdError {
return &EtcdError{
ErrorCode: errorCode,
Message: errorMap[errorCode],
Cause: cause,
Index: index,
}
}
func handleError(b []byte) error {
etcdErr := new(EtcdError)
err := json.Unmarshal(b, etcdErr)
if err != nil {
logger.Warningf("cannot unmarshal etcd error: %v", err)
return err
}
return etcdErr
}

View File

@@ -0,0 +1,27 @@
package etcd
// Get gets the file or directory associated with the given key.
// If the key points to a directory, files and directories under
// it will be returned in sorted or unsorted order, depending on
// the sort flag.
// If recursive is set to false, contents under child directories
// will not be returned.
// If recursive is set to true, all the contents will be returned.
func (c *Client) Get(key string, sort, recursive bool) (*Response, error) {
raw, err := c.RawGet(key, sort, recursive)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) {
ops := Options{
"recursive": recursive,
"sorted": sort,
}
return c.get(key, ops)
}

View File

@@ -0,0 +1,131 @@
package etcd
import (
"reflect"
"testing"
)
// cleanNode scrubs Expiration, ModifiedIndex and CreatedIndex of a node.
func cleanNode(n *Node) {
n.Expiration = nil
n.ModifiedIndex = 0
n.CreatedIndex = 0
}
// cleanResult scrubs a result object two levels deep of Expiration,
// ModifiedIndex and CreatedIndex.
func cleanResult(result *Response) {
// TODO(philips): make this recursive.
cleanNode(result.Node)
for i, _ := range result.Node.Nodes {
cleanNode(result.Node.Nodes[i])
for j, _ := range result.Node.Nodes[i].Nodes {
cleanNode(result.Node.Nodes[i].Nodes[j])
}
}
}
func TestGet(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
}()
c.Set("foo", "bar", 5)
result, err := c.Get("foo", false, false)
if err != nil {
t.Fatal(err)
}
if result.Node.Key != "/foo" || result.Node.Value != "bar" {
t.Fatalf("Get failed with %s %s %v", result.Node.Key, result.Node.Value, result.Node.TTL)
}
result, err = c.Get("goo", false, false)
if err == nil {
t.Fatalf("should not be able to get non-exist key")
}
}
func TestGetAll(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("fooDir", true)
}()
c.CreateDir("fooDir", 5)
c.Set("fooDir/k0", "v0", 5)
c.Set("fooDir/k1", "v1", 5)
// Return kv-pairs in sorted order
result, err := c.Get("fooDir", true, false)
if err != nil {
t.Fatal(err)
}
expected := Nodes{
&Node{
Key: "/fooDir/k0",
Value: "v0",
TTL: 5,
},
&Node{
Key: "/fooDir/k1",
Value: "v1",
TTL: 5,
},
}
cleanResult(result)
if !reflect.DeepEqual(result.Node.Nodes, expected) {
t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected)
}
// Test the `recursive` option
c.CreateDir("fooDir/childDir", 5)
c.Set("fooDir/childDir/k2", "v2", 5)
// Return kv-pairs in sorted order
result, err = c.Get("fooDir", true, true)
cleanResult(result)
if err != nil {
t.Fatal(err)
}
expected = Nodes{
&Node{
Key: "/fooDir/childDir",
Dir: true,
Nodes: Nodes{
&Node{
Key: "/fooDir/childDir/k2",
Value: "v2",
TTL: 5,
},
},
TTL: 5,
},
&Node{
Key: "/fooDir/k0",
Value: "v0",
TTL: 5,
},
&Node{
Key: "/fooDir/k1",
Value: "v1",
TTL: 5,
},
}
cleanResult(result)
if !reflect.DeepEqual(result.Node.Nodes, expected) {
t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected)
}
}

View File

@@ -0,0 +1,72 @@
package etcd
import (
"fmt"
"net/url"
"reflect"
)
type Options map[string]interface{}
// An internally-used data structure that represents a mapping
// between valid options and their kinds
type validOptions map[string]reflect.Kind
// Valid options for GET, PUT, POST, DELETE
// Using CAPITALIZED_UNDERSCORE to emphasize that these
// values are meant to be used as constants.
var (
VALID_GET_OPTIONS = validOptions{
"recursive": reflect.Bool,
"consistent": reflect.Bool,
"sorted": reflect.Bool,
"wait": reflect.Bool,
"waitIndex": reflect.Uint64,
}
VALID_PUT_OPTIONS = validOptions{
"prevValue": reflect.String,
"prevIndex": reflect.Uint64,
"prevExist": reflect.Bool,
"dir": reflect.Bool,
}
VALID_POST_OPTIONS = validOptions{}
VALID_DELETE_OPTIONS = validOptions{
"recursive": reflect.Bool,
"dir": reflect.Bool,
"prevValue": reflect.String,
"prevIndex": reflect.Uint64,
}
)
// Convert options to a string of HTML parameters
func (ops Options) toParameters(validOps validOptions) (string, error) {
p := "?"
values := url.Values{}
if ops == nil {
return "", nil
}
for k, v := range ops {
// Check if the given option is valid (that it exists)
kind := validOps[k]
if kind == reflect.Invalid {
return "", fmt.Errorf("Invalid option: %v", k)
}
// Check if the given option is of the valid type
t := reflect.TypeOf(v)
if kind != t.Kind() {
return "", fmt.Errorf("Option %s should be of %v kind, not of %v kind.",
k, kind, t.Kind())
}
values.Set(k, fmt.Sprintf("%v", v))
}
p += values.Encode()
return p, nil
}

View File

@@ -0,0 +1,365 @@
package etcd
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"path"
"strings"
"sync"
"time"
)
// Errors introduced by handling requests
var (
ErrRequestCancelled = errors.New("sending request is cancelled")
)
type RawRequest struct {
Method string
RelativePath string
Values url.Values
Cancel <-chan bool
}
// NewRawRequest returns a new RawRequest
func NewRawRequest(method, relativePath string, values url.Values, cancel <-chan bool) *RawRequest {
return &RawRequest{
Method: method,
RelativePath: relativePath,
Values: values,
Cancel: cancel,
}
}
// getCancelable issues a cancelable GET request
func (c *Client) getCancelable(key string, options Options,
cancel <-chan bool) (*RawResponse, error) {
logger.Debugf("get %s [%s]", key, c.cluster.Leader)
p := keyToPath(key)
// If consistency level is set to STRONG, append
// the `consistent` query string.
if c.config.Consistency == STRONG_CONSISTENCY {
options["consistent"] = true
}
str, err := options.toParameters(VALID_GET_OPTIONS)
if err != nil {
return nil, err
}
p += str
req := NewRawRequest("GET", p, nil, cancel)
resp, err := c.SendRequest(req)
if err != nil {
return nil, err
}
return resp, nil
}
// get issues a GET request
func (c *Client) get(key string, options Options) (*RawResponse, error) {
return c.getCancelable(key, options, nil)
}
// put issues a PUT request
func (c *Client) put(key string, value string, ttl uint64,
options Options) (*RawResponse, error) {
logger.Debugf("put %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader)
p := keyToPath(key)
str, err := options.toParameters(VALID_PUT_OPTIONS)
if err != nil {
return nil, err
}
p += str
req := NewRawRequest("PUT", p, buildValues(value, ttl), nil)
resp, err := c.SendRequest(req)
if err != nil {
return nil, err
}
return resp, nil
}
// post issues a POST request
func (c *Client) post(key string, value string, ttl uint64) (*RawResponse, error) {
logger.Debugf("post %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader)
p := keyToPath(key)
req := NewRawRequest("POST", p, buildValues(value, ttl), nil)
resp, err := c.SendRequest(req)
if err != nil {
return nil, err
}
return resp, nil
}
// delete issues a DELETE request
func (c *Client) delete(key string, options Options) (*RawResponse, error) {
logger.Debugf("delete %s [%s]", key, c.cluster.Leader)
p := keyToPath(key)
str, err := options.toParameters(VALID_DELETE_OPTIONS)
if err != nil {
return nil, err
}
p += str
req := NewRawRequest("DELETE", p, nil, nil)
resp, err := c.SendRequest(req)
if err != nil {
return nil, err
}
return resp, nil
}
// SendRequest sends a HTTP request and returns a Response as defined by etcd
func (c *Client) SendRequest(rr *RawRequest) (*RawResponse, error) {
var req *http.Request
var resp *http.Response
var httpPath string
var err error
var respBody []byte
reqs := make([]http.Request, 0)
resps := make([]http.Response, 0)
checkRetry := c.CheckRetry
if checkRetry == nil {
checkRetry = DefaultCheckRetry
}
cancelled := make(chan bool, 1)
reqLock := new(sync.Mutex)
if rr.Cancel != nil {
cancelRoutine := make(chan bool)
defer close(cancelRoutine)
go func() {
select {
case <-rr.Cancel:
cancelled <- true
logger.Debug("send.request is cancelled")
case <-cancelRoutine:
return
}
// Repeat canceling request until this thread is stopped
// because we have no idea about whether it succeeds.
for {
reqLock.Lock()
c.httpClient.Transport.(*http.Transport).CancelRequest(req)
reqLock.Unlock()
select {
case <-time.After(100 * time.Millisecond):
case <-cancelRoutine:
return
}
}
}()
}
// if we connect to a follower, we will retry until we find a leader
for attempt := 0; ; attempt++ {
select {
case <-cancelled:
return nil, ErrRequestCancelled
default:
}
logger.Debug("begin attempt", attempt, "for", rr.RelativePath)
if rr.Method == "GET" && c.config.Consistency == WEAK_CONSISTENCY {
// If it's a GET and consistency level is set to WEAK,
// then use a random machine.
httpPath = c.getHttpPath(true, rr.RelativePath)
} else {
// Else use the leader.
httpPath = c.getHttpPath(false, rr.RelativePath)
}
// Return a cURL command if curlChan is set
if c.cURLch != nil {
command := fmt.Sprintf("curl -X %s %s", rr.Method, httpPath)
for key, value := range rr.Values {
command += fmt.Sprintf(" -d %s=%s", key, value[0])
}
c.sendCURL(command)
}
logger.Debug("send.request.to ", httpPath, " | method ", rr.Method)
reqLock.Lock()
if rr.Values == nil {
if req, err = http.NewRequest(rr.Method, httpPath, nil); err != nil {
return nil, err
}
} else {
body := strings.NewReader(rr.Values.Encode())
if req, err = http.NewRequest(rr.Method, httpPath, body); err != nil {
return nil, err
}
req.Header.Set("Content-Type",
"application/x-www-form-urlencoded; param=value")
}
reqLock.Unlock()
resp, err = c.httpClient.Do(req)
// If the request was cancelled, return ErrRequestCancelled directly
select {
case <-cancelled:
return nil, ErrRequestCancelled
default:
}
reqs = append(reqs, *req)
// network error, change a machine!
if err != nil {
logger.Debug("network error:", err.Error())
resps = append(resps, http.Response{})
if checkErr := checkRetry(c.cluster, reqs, resps, err); checkErr != nil {
return nil, checkErr
}
c.cluster.switchLeader(attempt % len(c.cluster.Machines))
continue
}
// if there is no error, it should receive response
resps = append(resps, *resp)
defer resp.Body.Close()
logger.Debug("recv.response.from", httpPath)
if validHttpStatusCode[resp.StatusCode] {
// try to read byte code and break the loop
respBody, err = ioutil.ReadAll(resp.Body)
if err == nil {
logger.Debug("recv.success.", httpPath)
break
}
}
// if resp is TemporaryRedirect, set the new leader and retry
if resp.StatusCode == http.StatusTemporaryRedirect {
u, err := resp.Location()
if err != nil {
logger.Warning(err)
} else {
// Update cluster leader based on redirect location
// because it should point to the leader address
c.cluster.updateLeaderFromURL(u)
logger.Debug("recv.response.relocate", u.String())
}
continue
}
if checkErr := checkRetry(c.cluster, reqs, resps,
errors.New("Unexpected HTTP status code")); checkErr != nil {
return nil, checkErr
}
}
r := &RawResponse{
StatusCode: resp.StatusCode,
Body: respBody,
Header: resp.Header,
}
return r, nil
}
// DefaultCheckRetry checks retry cases
// If it has retried 2 * machine number, stop to retry it anymore
// If resp is nil, sleep for 200ms
// If status code is InternalServerError, sleep for 200ms.
func DefaultCheckRetry(cluster *Cluster, reqs []http.Request,
resps []http.Response, err error) error {
if len(reqs) >= 2*len(cluster.Machines) {
return newError(ErrCodeEtcdNotReachable,
"Tried to connect to each peer twice and failed", 0)
}
resp := &resps[len(resps)-1]
if resp == nil {
time.Sleep(time.Millisecond * 200)
return nil
}
code := resp.StatusCode
if code == http.StatusInternalServerError {
time.Sleep(time.Millisecond * 200)
}
logger.Warning("bad response status code", code)
return nil
}
func (c *Client) getHttpPath(random bool, s ...string) string {
var machine string
if random {
machine = c.cluster.Machines[rand.Intn(len(c.cluster.Machines))]
} else {
machine = c.cluster.Leader
}
fullPath := machine + "/" + version
for _, seg := range s {
fullPath = fullPath + "/" + seg
}
return fullPath
}
// buildValues builds a url.Values map according to the given value and ttl
func buildValues(value string, ttl uint64) url.Values {
v := url.Values{}
if value != "" {
v.Set("value", value)
}
if ttl > 0 {
v.Set("ttl", fmt.Sprintf("%v", ttl))
}
return v
}
// convert key string to http path exclude version
// for example: key[foo] -> path[keys/foo]
// key[/] -> path[keys/]
func keyToPath(key string) string {
p := path.Join("keys", key)
// corner case: if key is "/" or "//" ect
// path join will clear the tailing "/"
// we need to add it back
if p == "keys" {
p = "keys/"
}
return p
}

View File

@@ -0,0 +1,89 @@
package etcd
import (
"encoding/json"
"net/http"
"strconv"
"time"
)
const (
rawResponse = iota
normalResponse
)
type responseType int
type RawResponse struct {
StatusCode int
Body []byte
Header http.Header
}
var (
validHttpStatusCode = map[int]bool{
http.StatusCreated: true,
http.StatusOK: true,
http.StatusBadRequest: true,
http.StatusNotFound: true,
http.StatusPreconditionFailed: true,
http.StatusForbidden: true,
}
)
// Unmarshal parses RawResponse and stores the result in Response
func (rr *RawResponse) Unmarshal() (*Response, error) {
if rr.StatusCode != http.StatusOK && rr.StatusCode != http.StatusCreated {
return nil, handleError(rr.Body)
}
resp := new(Response)
err := json.Unmarshal(rr.Body, resp)
if err != nil {
return nil, err
}
// attach index and term to response
resp.EtcdIndex, _ = strconv.ParseUint(rr.Header.Get("X-Etcd-Index"), 10, 64)
resp.RaftIndex, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Index"), 10, 64)
resp.RaftTerm, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Term"), 10, 64)
return resp, nil
}
type Response struct {
Action string `json:"action"`
Node *Node `json:"node"`
PrevNode *Node `json:"prevNode,omitempty"`
EtcdIndex uint64 `json:"etcdIndex"`
RaftIndex uint64 `json:"raftIndex"`
RaftTerm uint64 `json:"raftTerm"`
}
type Node struct {
Key string `json:"key, omitempty"`
Value string `json:"value,omitempty"`
Dir bool `json:"dir,omitempty"`
Expiration *time.Time `json:"expiration,omitempty"`
TTL int64 `json:"ttl,omitempty"`
Nodes Nodes `json:"nodes,omitempty"`
ModifiedIndex uint64 `json:"modifiedIndex,omitempty"`
CreatedIndex uint64 `json:"createdIndex,omitempty"`
}
type Nodes []*Node
// interfaces for sorting
func (ns Nodes) Len() int {
return len(ns)
}
func (ns Nodes) Less(i, j int) bool {
return ns[i].Key < ns[j].Key
}
func (ns Nodes) Swap(i, j int) {
ns[i], ns[j] = ns[j], ns[i]
}

View File

@@ -0,0 +1,42 @@
package etcd
import (
"fmt"
"testing"
)
func TestSetCurlChan(t *testing.T) {
c := NewClient(nil)
c.OpenCURL()
defer func() {
c.Delete("foo", true)
}()
_, err := c.Set("foo", "bar", 5)
if err != nil {
t.Fatal(err)
}
expected := fmt.Sprintf("curl -X PUT %s/v2/keys/foo -d value=bar -d ttl=5",
c.cluster.Leader)
actual := c.RecvCURL()
if expected != actual {
t.Fatalf(`Command "%s" is not equal to expected value "%s"`,
actual, expected)
}
c.SetConsistency(STRONG_CONSISTENCY)
_, err = c.Get("foo", false, false)
if err != nil {
t.Fatal(err)
}
expected = fmt.Sprintf("curl -X GET %s/v2/keys/foo?consistent=true&recursive=false&sorted=false",
c.cluster.Leader)
actual = c.RecvCURL()
if expected != actual {
t.Fatalf(`Command "%s" is not equal to expected value "%s"`,
actual, expected)
}
}

View File

@@ -0,0 +1,137 @@
package etcd
// Set sets the given key to the given value.
// It will create a new key value pair or replace the old one.
// It will not replace a existing directory.
func (c *Client) Set(key string, value string, ttl uint64) (*Response, error) {
raw, err := c.RawSet(key, value, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// Set sets the given key to a directory.
// It will create a new directory or replace the old key value pair by a directory.
// It will not replace a existing directory.
func (c *Client) SetDir(key string, ttl uint64) (*Response, error) {
raw, err := c.RawSetDir(key, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// CreateDir creates a directory. It succeeds only if
// the given key does not yet exist.
func (c *Client) CreateDir(key string, ttl uint64) (*Response, error) {
raw, err := c.RawCreateDir(key, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// UpdateDir updates the given directory. It succeeds only if the
// given key already exists.
func (c *Client) UpdateDir(key string, ttl uint64) (*Response, error) {
raw, err := c.RawUpdateDir(key, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// Create creates a file with the given value under the given key. It succeeds
// only if the given key does not yet exist.
func (c *Client) Create(key string, value string, ttl uint64) (*Response, error) {
raw, err := c.RawCreate(key, value, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// CreateInOrder creates a file with a key that's guaranteed to be higher than other
// keys in the given directory. It is useful for creating queues.
func (c *Client) CreateInOrder(dir string, value string, ttl uint64) (*Response, error) {
raw, err := c.RawCreateInOrder(dir, value, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
// Update updates the given key to the given value. It succeeds only if the
// given key already exists.
func (c *Client) Update(key string, value string, ttl uint64) (*Response, error) {
raw, err := c.RawUpdate(key, value, ttl)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) {
ops := Options{
"prevExist": true,
"dir": true,
}
return c.put(key, "", ttl, ops)
}
func (c *Client) RawCreateDir(key string, ttl uint64) (*RawResponse, error) {
ops := Options{
"prevExist": false,
"dir": true,
}
return c.put(key, "", ttl, ops)
}
func (c *Client) RawSet(key string, value string, ttl uint64) (*RawResponse, error) {
return c.put(key, value, ttl, nil)
}
func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) {
ops := Options{
"dir": true,
}
return c.put(key, "", ttl, ops)
}
func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse, error) {
ops := Options{
"prevExist": true,
}
return c.put(key, value, ttl, ops)
}
func (c *Client) RawCreate(key string, value string, ttl uint64) (*RawResponse, error) {
ops := Options{
"prevExist": false,
}
return c.put(key, value, ttl, ops)
}
func (c *Client) RawCreateInOrder(dir string, value string, ttl uint64) (*RawResponse, error) {
return c.post(dir, value, ttl)
}

View File

@@ -0,0 +1,241 @@
package etcd
import (
"testing"
)
func TestSet(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
}()
resp, err := c.Set("foo", "bar", 5)
if err != nil {
t.Fatal(err)
}
if resp.Node.Key != "/foo" || resp.Node.Value != "bar" || resp.Node.TTL != 5 {
t.Fatalf("Set 1 failed: %#v", resp)
}
if resp.PrevNode != nil {
t.Fatalf("Set 1 PrevNode failed: %#v", resp)
}
resp, err = c.Set("foo", "bar2", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/foo" && resp.Node.Value == "bar2" && resp.Node.TTL == 5) {
t.Fatalf("Set 2 failed: %#v", resp)
}
if resp.PrevNode.Key != "/foo" || resp.PrevNode.Value != "bar" || resp.Node.TTL != 5 {
t.Fatalf("Set 2 PrevNode failed: %#v", resp)
}
}
func TestUpdate(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
c.Delete("nonexistent", true)
}()
resp, err := c.Set("foo", "bar", 5)
if err != nil {
t.Fatal(err)
}
// This should succeed.
resp, err = c.Update("foo", "wakawaka", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "update" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
t.Fatalf("Update 1 failed: %#v", resp)
}
if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.Node.TTL == 5) {
t.Fatalf("Update 1 prevValue failed: %#v", resp)
}
// This should fail because the key does not exist.
resp, err = c.Update("nonexistent", "whatever", 5)
if err == nil {
t.Fatalf("The key %v did not exist, so the update should have failed."+
"The response was: %#v", resp.Node.Key, resp)
}
}
func TestCreate(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("newKey", true)
}()
newKey := "/newKey"
newValue := "/newValue"
// This should succeed
resp, err := c.Create(newKey, newValue, 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "create" && resp.Node.Key == newKey &&
resp.Node.Value == newValue && resp.Node.TTL == 5) {
t.Fatalf("Create 1 failed: %#v", resp)
}
if resp.PrevNode != nil {
t.Fatalf("Create 1 PrevNode failed: %#v", resp)
}
// This should fail, because the key is already there
resp, err = c.Create(newKey, newValue, 5)
if err == nil {
t.Fatalf("The key %v did exist, so the creation should have failed."+
"The response was: %#v", resp.Node.Key, resp)
}
}
func TestCreateInOrder(t *testing.T) {
c := NewClient(nil)
dir := "/queue"
defer func() {
c.DeleteDir(dir)
}()
var firstKey, secondKey string
resp, err := c.CreateInOrder(dir, "1", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "create" && resp.Node.Value == "1" && resp.Node.TTL == 5) {
t.Fatalf("Create 1 failed: %#v", resp)
}
firstKey = resp.Node.Key
resp, err = c.CreateInOrder(dir, "2", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "create" && resp.Node.Value == "2" && resp.Node.TTL == 5) {
t.Fatalf("Create 2 failed: %#v", resp)
}
secondKey = resp.Node.Key
if firstKey >= secondKey {
t.Fatalf("Expected first key to be greater than second key, but %s is not greater than %s",
firstKey, secondKey)
}
}
func TestSetDir(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("foo", true)
c.Delete("fooDir", true)
}()
resp, err := c.CreateDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/fooDir" && resp.Node.Value == "" && resp.Node.TTL == 5) {
t.Fatalf("SetDir 1 failed: %#v", resp)
}
if resp.PrevNode != nil {
t.Fatalf("SetDir 1 PrevNode failed: %#v", resp)
}
// This should fail because /fooDir already points to a directory
resp, err = c.CreateDir("/fooDir", 5)
if err == nil {
t.Fatalf("fooDir already points to a directory, so SetDir should have failed."+
"The response was: %#v", resp)
}
_, err = c.Set("foo", "bar", 5)
if err != nil {
t.Fatal(err)
}
// This should succeed
// It should replace the key
resp, err = c.SetDir("foo", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/foo" && resp.Node.Value == "" && resp.Node.TTL == 5) {
t.Fatalf("SetDir 2 failed: %#v", resp)
}
if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.PrevNode.TTL == 5) {
t.Fatalf("SetDir 2 failed: %#v", resp)
}
}
func TestUpdateDir(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("fooDir", true)
}()
resp, err := c.CreateDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
// This should succeed.
resp, err = c.UpdateDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "update" && resp.Node.Key == "/fooDir" &&
resp.Node.Value == "" && resp.Node.TTL == 5) {
t.Fatalf("UpdateDir 1 failed: %#v", resp)
}
if !(resp.PrevNode.Key == "/fooDir" && resp.PrevNode.Dir == true && resp.PrevNode.TTL == 5) {
t.Fatalf("UpdateDir 1 PrevNode failed: %#v", resp)
}
// This should fail because the key does not exist.
resp, err = c.UpdateDir("nonexistentDir", 5)
if err == nil {
t.Fatalf("The key %v did not exist, so the update should have failed."+
"The response was: %#v", resp.Node.Key, resp)
}
}
func TestCreateDir(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("fooDir", true)
}()
// This should succeed
resp, err := c.CreateDir("fooDir", 5)
if err != nil {
t.Fatal(err)
}
if !(resp.Action == "create" && resp.Node.Key == "/fooDir" &&
resp.Node.Value == "" && resp.Node.TTL == 5) {
t.Fatalf("CreateDir 1 failed: %#v", resp)
}
if resp.PrevNode != nil {
t.Fatalf("CreateDir 1 PrevNode failed: %#v", resp)
}
// This should fail, because the key is already there
resp, err = c.CreateDir("fooDir", 5)
if err == nil {
t.Fatalf("The key %v did exist, so the creation should have failed."+
"The response was: %#v", resp.Node.Key, resp)
}
}

View File

@@ -0,0 +1,3 @@
package etcd
const version = "v2"

View File

@@ -0,0 +1,103 @@
package etcd
import (
"errors"
)
// Errors introduced by the Watch command.
var (
ErrWatchStoppedByUser = errors.New("Watch stopped by the user via stop channel")
)
// If recursive is set to true the watch returns the first change under the given
// prefix since the given index.
//
// If recursive is set to false the watch returns the first change to the given key
// since the given index.
//
// To watch for the latest change, set waitIndex = 0.
//
// If a receiver channel is given, it will be a long-term watch. Watch will block at the
//channel. After someone receives the channel, it will go on to watch that
// prefix. If a stop channel is given, the client can close long-term watch using
// the stop channel.
func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool,
receiver chan *Response, stop chan bool) (*Response, error) {
logger.Debugf("watch %s [%s]", prefix, c.cluster.Leader)
if receiver == nil {
raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
if err != nil {
return nil, err
}
return raw.Unmarshal()
}
defer close(receiver)
for {
raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
if err != nil {
return nil, err
}
resp, err := raw.Unmarshal()
if err != nil {
return nil, err
}
waitIndex = resp.Node.ModifiedIndex + 1
receiver <- resp
}
}
func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
receiver chan *RawResponse, stop chan bool) (*RawResponse, error) {
logger.Debugf("rawWatch %s [%s]", prefix, c.cluster.Leader)
if receiver == nil {
return c.watchOnce(prefix, waitIndex, recursive, stop)
}
for {
raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
if err != nil {
return nil, err
}
resp, err := raw.Unmarshal()
if err != nil {
return nil, err
}
waitIndex = resp.Node.ModifiedIndex + 1
receiver <- raw
}
}
// helper func
// return when there is change under the given prefix
func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop chan bool) (*RawResponse, error) {
options := Options{
"wait": true,
}
if waitIndex > 0 {
options["waitIndex"] = waitIndex
}
if recursive {
options["recursive"] = true
}
resp, err := c.getCancelable(key, options, stop)
if err == ErrRequestCancelled {
return nil, ErrWatchStoppedByUser
}
return resp, err
}

View File

@@ -0,0 +1,119 @@
package etcd
import (
"fmt"
"runtime"
"testing"
"time"
)
func TestWatch(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("watch_foo", true)
}()
go setHelper("watch_foo", "bar", c)
resp, err := c.Watch("watch_foo", 0, false, nil, nil)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") {
t.Fatalf("Watch 1 failed: %#v", resp)
}
go setHelper("watch_foo", "bar", c)
resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, false, nil, nil)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") {
t.Fatalf("Watch 2 failed: %#v", resp)
}
routineNum := runtime.NumGoroutine()
ch := make(chan *Response, 10)
stop := make(chan bool, 1)
go setLoop("watch_foo", "bar", c)
go receiver(ch, stop)
_, err = c.Watch("watch_foo", 0, false, ch, stop)
if err != ErrWatchStoppedByUser {
t.Fatalf("Watch returned a non-user stop error")
}
if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum {
t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum)
}
}
func TestWatchAll(t *testing.T) {
c := NewClient(nil)
defer func() {
c.Delete("watch_foo", true)
}()
go setHelper("watch_foo/foo", "bar", c)
resp, err := c.Watch("watch_foo", 0, true, nil, nil)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") {
t.Fatalf("WatchAll 1 failed: %#v", resp)
}
go setHelper("watch_foo/foo", "bar", c)
resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, true, nil, nil)
if err != nil {
t.Fatal(err)
}
if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") {
t.Fatalf("WatchAll 2 failed: %#v", resp)
}
ch := make(chan *Response, 10)
stop := make(chan bool, 1)
routineNum := runtime.NumGoroutine()
go setLoop("watch_foo/foo", "bar", c)
go receiver(ch, stop)
_, err = c.Watch("watch_foo", 0, true, ch, stop)
if err != ErrWatchStoppedByUser {
t.Fatalf("Watch returned a non-user stop error")
}
if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum {
t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum)
}
}
func setHelper(key, value string, c *Client) {
time.Sleep(time.Second)
c.Set(key, value, 100)
}
func setLoop(key, value string, c *Client) {
time.Sleep(time.Second)
for i := 0; i < 10; i++ {
newValue := fmt.Sprintf("%s_%v", value, i)
c.Set(key, newValue, 100)
time.Sleep(time.Second / 10)
}
}
func receiver(c chan *Response, stop chan bool) {
for i := 0; i < 10; i++ {
<-c
}
stop <- true
}

View File

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

View File

@@ -0,0 +1,121 @@
go-log
==========
go-log is a simple logging library for Go which supports logging to
systemd.
### Examples
#### Default
This example uses the default log to log to standard out and (if available) to systemd:
```go
package main
import (
"github.com/coreos/go-log/log"
)
func main() {
log.Info("Hello World.")
log.Error("There's nothing more to this program.")
}
```
#### Using Sinks and Formats
```go
package main
import (
"github.com/coreos/go-log/log"
"os"
)
func main() {
l := log.NewSimple(
log.WriterSink(os.Stderr,
"%s: %s[%d] %s\n",
[]string{"priority", "executable", "pid", "message"}))
l.Info("Here's a differently formatted log message.")
}
```
#### Custom Sink
This example only logs messages with priority `PriErr` and greater.
```go
package main
import (
"github.com/coreos/go-log/log"
"os"
)
func main() {
l := log.NewSimple(
&PriorityFilter{
log.PriErr,
log.WriterSink(os.Stdout, log.BasicFormat, log.BasicFields),
})
l.Info("This will be filtered out")
l.Info("and not printed at all.")
l.Error("This will be printed, though!")
l.Critical("And so will this!")
}
type PriorityFilter struct {
priority log.Priority
target log.Sink
}
func (filter *PriorityFilter) Log(fields log.Fields) {
// lower priority values indicate more important messages
if fields["priority"].(log.Priority) <= filter.priority {
filter.target.Log(fields)
}
}
```
### Fields
The following fields are available for use in all sinks:
```go
"prefix" string // static field available to all sinks
"seq" uint64 // auto-incrementing sequence number
"start_time" time.Time // start time of the log
"time" string // formatted time of log entry
"full_time" time.Time // time of log entry
"rtime" time.Duration // relative time of log entry since started
"pid" int // process id
"executable" string // executable filename
```
In addition, if `verbose=true` is passed to `New()`, the following (somewhat expensive) runtime fields are also available:
```go
"funcname" string // function name where the log function was called
"lineno" int // line number where the log function was called
"pathname" string // full pathname of caller
"filename" string // filename of caller
```
### Logging functions
All these functions can also be called directly to use the default log.
```go
func (*Logger) Log(priority Priority, v ...interface)
func (*Logger) Logf(priority Priority, format string, v ...interface{})
func (*Logger) Emergency(v ...interface)
func (*Logger) Emergencyf(format string, v ...interface{})
func (*Logger) Alert(v ...interface)
func (*Logger) Alertf(format string, v ...interface{})
func (*Logger) Critical(v ...interface)
func (*Logger) Criticalf(format string, v ...interface{})
func (*Logger) Error(v ...interface)
func (*Logger) Errorf(format string, v ...interface{})
func (*Logger) Warning(v ...interface)
func (*Logger) Warningf(format string, v ...interface{})
func (*Logger) Notice(v ...interface)
func (*Logger) Noticef(format string, v ...interface{})
func (*Logger) Info(v ...interface)
func (*Logger) Infof(format string, v ...interface{})
func (*Logger) Debug(v ...interface)
func (*Logger) Debugf(format string, v ...interface{})
```
### Acknowledgements
This package is a mostly-from-scratch rewrite of
[ccding/go-logging](https://github.com/ccding/go-logging) with some features
removed and systemd support added.

View File

@@ -0,0 +1,214 @@
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"fmt"
"os"
)
var BasicFormat = "%s [%9s] %s- %s\n"
var BasicFields = []string{"time", "priority", "prefix", "message"}
var RichFormat = "%s [%9s] %d %s - %s:%s:%d - %s\n"
var RichFields = []string{"full_time", "priority", "seq", "prefix", "filename", "funcname", "lineno", "message"}
// This function has an unusual name to aid in finding it while walking the
// stack. We need to do some dead reckoning from this function to access the
// caller's stack, so there is a consistent call depth above this function.
func (logger *Logger) Log(priority Priority, v ...interface{}) {
fields := logger.fieldValues()
fields["priority"] = priority
fields["message"] = fmt.Sprint(v...)
for _, sink := range logger.sinks {
sink.Log(fields)
}
}
func (logger *Logger) Logf(priority Priority, format string, v ...interface{}) {
logger.Log(priority, fmt.Sprintf(format, v...))
}
func (logger *Logger) Emergency(v ...interface{}) {
logger.Log(PriEmerg, v...)
}
func (logger *Logger) Emergencyf(format string, v ...interface{}) {
logger.Log(PriEmerg, fmt.Sprintf(format, v...))
}
func (logger *Logger) Alert(v ...interface{}) {
logger.Log(PriAlert, v...)
}
func (logger *Logger) Alertf(format string, v ...interface{}) {
logger.Log(PriAlert, fmt.Sprintf(format, v...))
}
func (logger *Logger) Critical(v ...interface{}) {
logger.Log(PriCrit, v...)
}
func (logger *Logger) Criticalf(format string, v ...interface{}) {
logger.Log(PriCrit, fmt.Sprintf(format, v...))
}
func (logger *Logger) Error(v ...interface{}) {
logger.Log(PriErr, v...)
}
func (logger *Logger) Errorf(format string, v ...interface{}) {
logger.Log(PriErr, fmt.Sprintf(format, v...))
}
func (logger *Logger) Warning(v ...interface{}) {
logger.Log(PriWarning, v...)
}
func (logger *Logger) Warningf(format string, v ...interface{}) {
logger.Log(PriWarning, fmt.Sprintf(format, v...))
}
func (logger *Logger) Notice(v ...interface{}) {
logger.Log(PriNotice, v...)
}
func (logger *Logger) Noticef(format string, v ...interface{}) {
logger.Log(PriNotice, fmt.Sprintf(format, v...))
}
func (logger *Logger) Info(v ...interface{}) {
logger.Log(PriInfo, v...)
}
func (logger *Logger) Infof(format string, v ...interface{}) {
logger.Log(PriInfo, fmt.Sprintf(format, v...))
}
func (logger *Logger) Debug(v ...interface{}) {
logger.Log(PriDebug, v...)
}
func (logger *Logger) Debugf(format string, v ...interface{}) {
logger.Log(PriDebug, fmt.Sprintf(format, v...))
}
func Emergency(v ...interface{}) {
defaultLogger.Log(PriEmerg, v...)
}
func Emergencyf(format string, v ...interface{}) {
defaultLogger.Log(PriEmerg, fmt.Sprintf(format, v...))
}
func Alert(v ...interface{}) {
defaultLogger.Log(PriAlert, v...)
}
func Alertf(format string, v ...interface{}) {
defaultLogger.Log(PriAlert, fmt.Sprintf(format, v...))
}
func Critical(v ...interface{}) {
defaultLogger.Log(PriCrit, v...)
}
func Criticalf(format string, v ...interface{}) {
defaultLogger.Log(PriCrit, fmt.Sprintf(format, v...))
}
func Error(v ...interface{}) {
defaultLogger.Log(PriErr, v...)
}
func Errorf(format string, v ...interface{}) {
defaultLogger.Log(PriErr, fmt.Sprintf(format, v...))
}
func Warning(v ...interface{}) {
defaultLogger.Log(PriWarning, v...)
}
func Warningf(format string, v ...interface{}) {
defaultLogger.Log(PriWarning, fmt.Sprintf(format, v...))
}
func Notice(v ...interface{}) {
defaultLogger.Log(PriNotice, v...)
}
func Noticef(format string, v ...interface{}) {
defaultLogger.Log(PriNotice, fmt.Sprintf(format, v...))
}
func Info(v ...interface{}) {
defaultLogger.Log(PriInfo, v...)
}
func Infof(format string, v ...interface{}) {
defaultLogger.Log(PriInfo, fmt.Sprintf(format, v...))
}
func Debug(v ...interface{}) {
defaultLogger.Log(PriDebug, v...)
}
func Debugf(format string, v ...interface{}) {
defaultLogger.Log(PriDebug, fmt.Sprintf(format, v...))
}
// Standard library log functions
func (logger *Logger)Fatalln (v ...interface{}) {
logger.Log(PriCrit, v...)
os.Exit(1)
}
func (logger *Logger)Fatalf (format string, v ...interface{}) {
logger.Logf(PriCrit, format, v...)
os.Exit(1)
}
func (logger *Logger)Panicln (v ...interface{}) {
s := fmt.Sprint(v...)
logger.Log(PriErr, s)
panic(s)
}
func (logger *Logger)Panicf (format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)
logger.Log(PriErr, s)
panic(s)
}
func (logger *Logger)Println (v ...interface{}) {
logger.Log(PriInfo, v...)
}
func (logger *Logger)Printf (format string, v ...interface{}) {
logger.Logf(PriInfo, format, v...)
}
func Fatalln (v ...interface{}) {
defaultLogger.Log(PriCrit, v...)
os.Exit(1)
}
func Fatalf (format string, v ...interface{}) {
defaultLogger.Logf(PriCrit, format, v...)
os.Exit(1)
}
func Panicln (v ...interface{}) {
s := fmt.Sprint(v...)
defaultLogger.Log(PriErr, s)
panic(s)
}
func Panicf (format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)
defaultLogger.Log(PriErr, s)
panic(s)
}
func Println (v ...interface{}) {
defaultLogger.Log(PriInfo, v...)
}
func Printf (format string, v ...interface{}) {
defaultLogger.Logf(PriInfo, format, v...)
}

View File

@@ -0,0 +1,69 @@
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"os"
"path"
"runtime"
"strings"
"sync/atomic"
"time"
)
type Fields map[string]interface{}
func (logger *Logger) fieldValues() Fields {
now := time.Now()
fields := Fields{
"prefix": logger.prefix, // static field available to all sinks
"seq": logger.nextSeq(), // auto-incrementing sequence number
"start_time": logger.created, // start time of the logger
"time": now.Format(time.StampMilli), // formatted time of log entry
"full_time": now, // time of log entry
"rtime": time.Since(logger.created), // relative time of log entry since started
"pid": os.Getpid(), // process id
"executable": logger.executable, // executable filename
}
if logger.verbose {
setVerboseFields(fields)
}
return fields
}
func (logger *Logger) nextSeq() uint64 {
return atomic.AddUint64(&logger.seq, 1)
}
func setVerboseFields(fields Fields) {
callers := make([]uintptr, 10)
n := runtime.Callers(3, callers) // starts in (*Logger).Log or similar
callers = callers[:n]
for _, pc := range callers {
f := runtime.FuncForPC(pc)
if !strings.Contains(f.Name(), "logger.(*Logger)") {
fields["funcname"] = f.Name()
pathname, lineno := f.FileLine(pc)
fields["lineno"] = lineno
fields["pathname"] = pathname
fields["filename"] = path.Base(pathname)
return
}
}
}

View File

@@ -0,0 +1,72 @@
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"bitbucket.org/kardianos/osext"
"os"
"path"
"time"
)
// Logger is user-immutable immutable struct which can log to several outputs
type Logger struct {
sinks []Sink // the sinks this logger will log to
verbose bool // gather expensive logging data?
prefix string // static field available to all log sinks under this logger
created time.Time // time when this logger was created
seq uint64 // sequential number of log message, starting at 1
executable string // executable name
}
// New creates a new Logger which logs to all the supplied sinks. The prefix
// argument is passed to all loggers under the field "prefix" with every log
// message. If verbose is true, more expensive runtime fields will be computed
// and passed to loggers. These fields are funcname, lineno, pathname, and
// filename.
func New(prefix string, verbose bool, sinks ...Sink) *Logger {
return &Logger{
sinks: sinks,
verbose: verbose,
prefix: prefix,
created: time.Now(),
seq: 0,
executable: getExecutableName(),
}
}
func getExecutableName() string {
executablePath, err := osext.Executable()
if err != nil {
return "(UNKNOWN)"
} else {
return path.Base(executablePath)
}
}
// NewSimple(sinks...) is equivalent to New("", false, sinks...)
func NewSimple(sinks ...Sink) *Logger {
return New("", false, sinks...)
}
var defaultLogger *Logger
func init() {
defaultLogger = NewSimple(CombinedSink(os.Stdout, BasicFormat, BasicFields))
}

View File

@@ -0,0 +1,54 @@
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
type Priority int
const (
PriEmerg Priority = iota
PriAlert
PriCrit
PriErr
PriWarning
PriNotice
PriInfo
PriDebug
)
func (priority Priority) String() string {
switch priority {
case PriEmerg:
return "EMERGENCY"
case PriAlert:
return "ALERT"
case PriCrit:
return "CRITICAL"
case PriErr:
return "ERROR"
case PriWarning:
return "WARNING"
case PriNotice:
return "NOTICE"
case PriInfo:
return "INFO"
case PriDebug:
return "DEBUG"
default:
return "UNKNOWN"
}
}

View File

@@ -0,0 +1,97 @@
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"fmt"
"io"
"sync"
)
const AsyncBuffer = 100
type Sink interface {
Log(Fields)
}
type nullSink struct{}
func (sink *nullSink) Log(fields Fields) {}
func NullSink() Sink {
return &nullSink{}
}
type writerSink struct {
lock sync.Mutex
out io.Writer
format string
fields []string
}
func (sink *writerSink) Log(fields Fields) {
vals := make([]interface{}, len(sink.fields))
for i, field := range sink.fields {
var ok bool
vals[i], ok = fields[field]
if !ok {
vals[i] = "???"
}
}
sink.lock.Lock()
defer sink.lock.Unlock()
fmt.Fprintf(sink.out, sink.format, vals...)
}
func WriterSink(out io.Writer, format string, fields []string) Sink {
return &writerSink{
out: out,
format: format,
fields: fields,
}
}
type combinedSink struct {
sinks []Sink
}
func (sink *combinedSink) Log(fields Fields) {
for _, s := range sink.sinks {
s.Log(fields)
}
}
type priorityFilter struct {
priority Priority
target Sink
}
func (filter *priorityFilter) Log(fields Fields) {
// lower priority values indicate more important messages
if fields["priority"].(Priority) <= filter.priority {
filter.target.Log(fields)
}
}
func PriorityFilter(priority Priority, target Sink) Sink {
return &priorityFilter{
priority: priority,
target: target,
}
}

View File

@@ -0,0 +1,82 @@
// +build !windows
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"fmt"
"github.com/coreos/go-systemd/journal"
"io"
"strings"
)
type journalSink struct{}
func (sink *journalSink) Log(fields Fields) {
message := fields["message"].(string)
priority := toJournalPriority(fields["priority"].(Priority))
journalFields := make(map[string]string)
for k, v := range fields {
if k == "message" || k == "priority" {
continue
}
journalFields[strings.ToUpper(k)] = fmt.Sprint(v)
}
journal.Send(message, priority, journalFields)
}
func toJournalPriority(priority Priority) journal.Priority {
switch priority {
case PriEmerg:
return journal.PriEmerg
case PriAlert:
return journal.PriAlert
case PriCrit:
return journal.PriCrit
case PriErr:
return journal.PriErr
case PriWarning:
return journal.PriWarning
case PriNotice:
return journal.PriNotice
case PriInfo:
return journal.PriInfo
case PriDebug:
return journal.PriDebug
default:
return journal.PriErr
}
}
func JournalSink() Sink {
return &journalSink{}
}
func CombinedSink(writer io.Writer, format string, fields []string) Sink {
sinks := make([]Sink, 0)
sinks = append(sinks, WriterSink(writer, format, fields))
if journal.Enabled() {
sinks = append(sinks, JournalSink())
}
return &combinedSink{
sinks: sinks,
}
}

View File

@@ -0,0 +1,33 @@
// +build windows
package log
// Copyright 2013, CoreOS, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// author: David Fisher <ddf1991@gmail.com>
// based on previous package by: Cong Ding <dinggnu@gmail.com>
import (
"io"
)
func CombinedSink(writer io.Writer, format string, fields []string) Sink {
sinks := make([]Sink, 0)
sinks = append(sinks, WriterSink(writer, format, fields))
return &combinedSink{
sinks: sinks,
}
}

View File

@@ -0,0 +1,168 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package journal provides write bindings to the systemd journal
package journal
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"strconv"
"strings"
"syscall"
)
// Priority of a journal message
type Priority int
const (
PriEmerg Priority = iota
PriAlert
PriCrit
PriErr
PriWarning
PriNotice
PriInfo
PriDebug
)
var conn net.Conn
func init() {
var err error
conn, err = net.Dial("unixgram", "/run/systemd/journal/socket")
if err != nil {
conn = nil
}
}
// Enabled returns true iff the systemd journal is available for logging
func Enabled() bool {
return conn != nil
}
// Send a message to the systemd journal. vars is a map of journald fields to
// values. Fields must be composed of uppercase letters, numbers, and
// underscores, but must not start with an underscore. Within these
// restrictions, any arbitrary field name may be used. Some names have special
// significance: see the journalctl documentation
// (http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
// for more details. vars may be nil.
func Send(message string, priority Priority, vars map[string]string) error {
if conn == nil {
return journalError("could not connect to journald socket")
}
data := new(bytes.Buffer)
appendVariable(data, "PRIORITY", strconv.Itoa(int(priority)))
appendVariable(data, "MESSAGE", message)
for k, v := range vars {
appendVariable(data, k, v)
}
_, err := io.Copy(conn, data)
if err != nil && isSocketSpaceError(err) {
file, err := tempFd()
if err != nil {
return journalError(err.Error())
}
_, err = io.Copy(file, data)
if err != nil {
return journalError(err.Error())
}
rights := syscall.UnixRights(int(file.Fd()))
/* this connection should always be a UnixConn, but better safe than sorry */
unixConn, ok := conn.(*net.UnixConn)
if !ok {
return journalError("can't send file through non-Unix connection")
}
unixConn.WriteMsgUnix([]byte{}, rights, nil)
} else if err != nil {
return journalError(err.Error())
}
return nil
}
func appendVariable(w io.Writer, name, value string) {
if !validVarName(name) {
journalError("variable name contains invalid character, ignoring")
}
if strings.ContainsRune(value, '\n') {
/* When the value contains a newline, we write:
* - the variable name, followed by a newline
* - the size (in 64bit little endian format)
* - the data, followed by a newline
*/
fmt.Fprintln(w, name)
binary.Write(w, binary.LittleEndian, uint64(len(value)))
fmt.Fprintln(w, value)
} else {
/* just write the variable and value all on one line */
fmt.Fprintf(w, "%s=%s\n", name, value)
}
}
func validVarName(name string) bool {
/* The variable name must be in uppercase and consist only of characters,
* numbers and underscores, and may not begin with an underscore. (from the docs)
*/
valid := name[0] != '_'
for _, c := range name {
valid = valid && ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_'
}
return valid
}
func isSocketSpaceError(err error) bool {
opErr, ok := err.(*net.OpError)
if !ok {
return false
}
sysErr, ok := opErr.Err.(syscall.Errno)
if !ok {
return false
}
return sysErr == syscall.EMSGSIZE || sysErr == syscall.ENOBUFS
}
func tempFd() (*os.File, error) {
file, err := ioutil.TempFile("/dev/shm/", "journal.XXXXX")
if err != nil {
return nil, err
}
syscall.Unlink(file.Name())
if err != nil {
return nil, err
}
return file, nil
}
func journalError(s string) error {
s = "journal error: " + s
fmt.Fprintln(os.Stderr, s)
return errors.New(s)
}

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")
}

View File

@@ -0,0 +1,185 @@
This software is licensed under the LGPLv3, included below.
As a special exception to the GNU Lesser General Public License version 3
("LGPL3"), the copyright holders of this Library give you permission to
convey to a third party a Combined Work that links statically or dynamically
to this Library without providing any Minimal Corresponding Source or
Minimal Application Code as set out in 4d or providing the installation
information set out in section 4e, provided that you comply with the other
provisions of LGPL3 and provided that you meet, for the Application the
terms and conditions of the license(s) which apply to the Application.
Except as stated in this special exception, the provisions of LGPL3 will
continue to comply in full to this Library. If you modify this Library, you
may apply this exception to your version of this Library, but you are not
obliged to do so. If you do not wish to do so, delete this exception
statement from your version. This exception does not (and cannot) modify any
license terms which apply to the Application, with which you must still
comply.
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2006 Kirill Simonov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,127 @@
# YAML support for the Go language
Introduction
------------
The yaml package enables Go programs to comfortably encode and decode YAML
values. It was developed within [Canonical](https://www.canonical.com) as
part of the [juju](https://juju.ubuntu.com) project, and is based on a
pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML)
C library to parse and generate YAML data quickly and reliably.
Compatibility
-------------
The yaml package is almost compatible with YAML 1.1, including support for
anchors, tags, etc. There are still a few missing bits, such as document
merging, base-60 floats (huh?), and multi-document unmarshalling. These
features are not hard to add, and will be introduced as necessary.
Installation and usage
----------------------
The import path for the package is *gopkg.in/yaml.v1*.
To install it, run:
go get gopkg.in/yaml.v1
API documentation
-----------------
If opened in a browser, the import path itself leads to the API documentation:
* [https://gopkg.in/yaml.v1](https://gopkg.in/yaml.v1)
API stability
-------------
The package API for yaml v1 will remain stable as described in [gopkg.in](https://gopkg.in).
License
-------
The yaml package is licensed under the LGPL with an exception that allows it to be linked statically. Please see the LICENSE file for details.
Example
-------
```Go
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v1"
)
var data = `
a: Easy!
b:
c: 2
d: [3, 4]
`
type T struct {
A string
B struct{C int; D []int ",flow"}
}
func main() {
t := T{}
err := yaml.Unmarshal([]byte(data), &t)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- t:\n%v\n\n", t)
d, err := yaml.Marshal(&t)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- t dump:\n%s\n\n", string(d))
m := make(map[interface{}]interface{})
err = yaml.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- m:\n%v\n\n", m)
d, err = yaml.Marshal(&m)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- m dump:\n%s\n\n", string(d))
}
```
This example will generate the following output:
```
--- t:
{Easy! {2 [3 4]}}
--- t dump:
a: Easy!
b:
c: 2
d: [3, 4]
--- m:
map[a:Easy! b:map[c:2 d:[3 4]]]
--- m dump:
a: Easy!
b:
c: 2
d:
- 3
- 4
```

View File

@@ -0,0 +1,742 @@
package yaml
import (
"io"
"os"
)
func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) {
//fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens))
// Check if we can move the queue at the beginning of the buffer.
if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) {
if parser.tokens_head != len(parser.tokens) {
copy(parser.tokens, parser.tokens[parser.tokens_head:])
}
parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head]
parser.tokens_head = 0
}
parser.tokens = append(parser.tokens, *token)
if pos < 0 {
return
}
copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:])
parser.tokens[parser.tokens_head+pos] = *token
}
// Create a new parser object.
func yaml_parser_initialize(parser *yaml_parser_t) bool {
*parser = yaml_parser_t{
raw_buffer: make([]byte, 0, input_raw_buffer_size),
buffer: make([]byte, 0, input_buffer_size),
}
return true
}
// Destroy a parser object.
func yaml_parser_delete(parser *yaml_parser_t) {
*parser = yaml_parser_t{}
}
// String read handler.
func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
if parser.input_pos == len(parser.input) {
return 0, io.EOF
}
n = copy(buffer, parser.input[parser.input_pos:])
parser.input_pos += n
return n, nil
}
// File read handler.
func yaml_file_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
return parser.input_file.Read(buffer)
}
// Set a string input.
func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) {
if parser.read_handler != nil {
panic("must set the input source only once")
}
parser.read_handler = yaml_string_read_handler
parser.input = input
parser.input_pos = 0
}
// Set a file input.
func yaml_parser_set_input_file(parser *yaml_parser_t, file *os.File) {
if parser.read_handler != nil {
panic("must set the input source only once")
}
parser.read_handler = yaml_file_read_handler
parser.input_file = file
}
// Set the source encoding.
func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) {
if parser.encoding != yaml_ANY_ENCODING {
panic("must set the encoding only once")
}
parser.encoding = encoding
}
// Create a new emitter object.
func yaml_emitter_initialize(emitter *yaml_emitter_t) bool {
*emitter = yaml_emitter_t{
buffer: make([]byte, output_buffer_size),
raw_buffer: make([]byte, 0, output_raw_buffer_size),
states: make([]yaml_emitter_state_t, 0, initial_stack_size),
events: make([]yaml_event_t, 0, initial_queue_size),
}
return true
}
// Destroy an emitter object.
func yaml_emitter_delete(emitter *yaml_emitter_t) {
*emitter = yaml_emitter_t{}
}
// String write handler.
func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
*emitter.output_buffer = append(*emitter.output_buffer, buffer...)
return nil
}
// File write handler.
func yaml_file_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
_, err := emitter.output_file.Write(buffer)
return err
}
// Set a string output.
func yaml_emitter_set_output_string(emitter *yaml_emitter_t, output_buffer *[]byte) {
if emitter.write_handler != nil {
panic("must set the output target only once")
}
emitter.write_handler = yaml_string_write_handler
emitter.output_buffer = output_buffer
}
// Set a file output.
func yaml_emitter_set_output_file(emitter *yaml_emitter_t, file io.Writer) {
if emitter.write_handler != nil {
panic("must set the output target only once")
}
emitter.write_handler = yaml_file_write_handler
emitter.output_file = file
}
// Set the output encoding.
func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) {
if emitter.encoding != yaml_ANY_ENCODING {
panic("must set the output encoding only once")
}
emitter.encoding = encoding
}
// Set the canonical output style.
func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) {
emitter.canonical = canonical
}
//// Set the indentation increment.
func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) {
if indent < 2 || indent > 9 {
indent = 2
}
emitter.best_indent = indent
}
// Set the preferred line width.
func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) {
if width < 0 {
width = -1
}
emitter.best_width = width
}
// Set if unescaped non-ASCII characters are allowed.
func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) {
emitter.unicode = unicode
}
// Set the preferred line break character.
func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) {
emitter.line_break = line_break
}
///*
// * Destroy a token object.
// */
//
//YAML_DECLARE(void)
//yaml_token_delete(yaml_token_t *token)
//{
// assert(token); // Non-NULL token object expected.
//
// switch (token.type)
// {
// case YAML_TAG_DIRECTIVE_TOKEN:
// yaml_free(token.data.tag_directive.handle);
// yaml_free(token.data.tag_directive.prefix);
// break;
//
// case YAML_ALIAS_TOKEN:
// yaml_free(token.data.alias.value);
// break;
//
// case YAML_ANCHOR_TOKEN:
// yaml_free(token.data.anchor.value);
// break;
//
// case YAML_TAG_TOKEN:
// yaml_free(token.data.tag.handle);
// yaml_free(token.data.tag.suffix);
// break;
//
// case YAML_SCALAR_TOKEN:
// yaml_free(token.data.scalar.value);
// break;
//
// default:
// break;
// }
//
// memset(token, 0, sizeof(yaml_token_t));
//}
//
///*
// * Check if a string is a valid UTF-8 sequence.
// *
// * Check 'reader.c' for more details on UTF-8 encoding.
// */
//
//static int
//yaml_check_utf8(yaml_char_t *start, size_t length)
//{
// yaml_char_t *end = start+length;
// yaml_char_t *pointer = start;
//
// while (pointer < end) {
// unsigned char octet;
// unsigned int width;
// unsigned int value;
// size_t k;
//
// octet = pointer[0];
// width = (octet & 0x80) == 0x00 ? 1 :
// (octet & 0xE0) == 0xC0 ? 2 :
// (octet & 0xF0) == 0xE0 ? 3 :
// (octet & 0xF8) == 0xF0 ? 4 : 0;
// value = (octet & 0x80) == 0x00 ? octet & 0x7F :
// (octet & 0xE0) == 0xC0 ? octet & 0x1F :
// (octet & 0xF0) == 0xE0 ? octet & 0x0F :
// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0;
// if (!width) return 0;
// if (pointer+width > end) return 0;
// for (k = 1; k < width; k ++) {
// octet = pointer[k];
// if ((octet & 0xC0) != 0x80) return 0;
// value = (value << 6) + (octet & 0x3F);
// }
// if (!((width == 1) ||
// (width == 2 && value >= 0x80) ||
// (width == 3 && value >= 0x800) ||
// (width == 4 && value >= 0x10000))) return 0;
//
// pointer += width;
// }
//
// return 1;
//}
//
// Create STREAM-START.
func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) bool {
*event = yaml_event_t{
typ: yaml_STREAM_START_EVENT,
encoding: encoding,
}
return true
}
// Create STREAM-END.
func yaml_stream_end_event_initialize(event *yaml_event_t) bool {
*event = yaml_event_t{
typ: yaml_STREAM_END_EVENT,
}
return true
}
// Create DOCUMENT-START.
func yaml_document_start_event_initialize(event *yaml_event_t, version_directive *yaml_version_directive_t,
tag_directives []yaml_tag_directive_t, implicit bool) bool {
*event = yaml_event_t{
typ: yaml_DOCUMENT_START_EVENT,
version_directive: version_directive,
tag_directives: tag_directives,
implicit: implicit,
}
return true
}
// Create DOCUMENT-END.
func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) bool {
*event = yaml_event_t{
typ: yaml_DOCUMENT_END_EVENT,
implicit: implicit,
}
return true
}
///*
// * Create ALIAS.
// */
//
//YAML_DECLARE(int)
//yaml_alias_event_initialize(event *yaml_event_t, anchor *yaml_char_t)
//{
// mark yaml_mark_t = { 0, 0, 0 }
// anchor_copy *yaml_char_t = NULL
//
// assert(event) // Non-NULL event object is expected.
// assert(anchor) // Non-NULL anchor is expected.
//
// if (!yaml_check_utf8(anchor, strlen((char *)anchor))) return 0
//
// anchor_copy = yaml_strdup(anchor)
// if (!anchor_copy)
// return 0
//
// ALIAS_EVENT_INIT(*event, anchor_copy, mark, mark)
//
// return 1
//}
// Create SCALAR.
func yaml_scalar_event_initialize(event *yaml_event_t, anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style yaml_scalar_style_t) bool {
*event = yaml_event_t{
typ: yaml_SCALAR_EVENT,
anchor: anchor,
tag: tag,
value: value,
implicit: plain_implicit,
quoted_implicit: quoted_implicit,
style: yaml_style_t(style),
}
return true
}
// Create SEQUENCE-START.
func yaml_sequence_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_sequence_style_t) bool {
*event = yaml_event_t{
typ: yaml_SEQUENCE_START_EVENT,
anchor: anchor,
tag: tag,
implicit: implicit,
style: yaml_style_t(style),
}
return true
}
// Create SEQUENCE-END.
func yaml_sequence_end_event_initialize(event *yaml_event_t) bool {
*event = yaml_event_t{
typ: yaml_SEQUENCE_END_EVENT,
}
return true
}
// Create MAPPING-START.
func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) bool {
*event = yaml_event_t{
typ: yaml_MAPPING_START_EVENT,
anchor: anchor,
tag: tag,
implicit: implicit,
style: yaml_style_t(style),
}
return true
}
// Create MAPPING-END.
func yaml_mapping_end_event_initialize(event *yaml_event_t) bool {
*event = yaml_event_t{
typ: yaml_MAPPING_END_EVENT,
}
return true
}
// Destroy an event object.
func yaml_event_delete(event *yaml_event_t) {
*event = yaml_event_t{}
}
///*
// * Create a document object.
// */
//
//YAML_DECLARE(int)
//yaml_document_initialize(document *yaml_document_t,
// version_directive *yaml_version_directive_t,
// tag_directives_start *yaml_tag_directive_t,
// tag_directives_end *yaml_tag_directive_t,
// start_implicit int, end_implicit int)
//{
// struct {
// error yaml_error_type_t
// } context
// struct {
// start *yaml_node_t
// end *yaml_node_t
// top *yaml_node_t
// } nodes = { NULL, NULL, NULL }
// version_directive_copy *yaml_version_directive_t = NULL
// struct {
// start *yaml_tag_directive_t
// end *yaml_tag_directive_t
// top *yaml_tag_directive_t
// } tag_directives_copy = { NULL, NULL, NULL }
// value yaml_tag_directive_t = { NULL, NULL }
// mark yaml_mark_t = { 0, 0, 0 }
//
// assert(document) // Non-NULL document object is expected.
// assert((tag_directives_start && tag_directives_end) ||
// (tag_directives_start == tag_directives_end))
// // Valid tag directives are expected.
//
// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error
//
// if (version_directive) {
// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t))
// if (!version_directive_copy) goto error
// version_directive_copy.major = version_directive.major
// version_directive_copy.minor = version_directive.minor
// }
//
// if (tag_directives_start != tag_directives_end) {
// tag_directive *yaml_tag_directive_t
// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE))
// goto error
// for (tag_directive = tag_directives_start
// tag_directive != tag_directives_end; tag_directive ++) {
// assert(tag_directive.handle)
// assert(tag_directive.prefix)
// if (!yaml_check_utf8(tag_directive.handle,
// strlen((char *)tag_directive.handle)))
// goto error
// if (!yaml_check_utf8(tag_directive.prefix,
// strlen((char *)tag_directive.prefix)))
// goto error
// value.handle = yaml_strdup(tag_directive.handle)
// value.prefix = yaml_strdup(tag_directive.prefix)
// if (!value.handle || !value.prefix) goto error
// if (!PUSH(&context, tag_directives_copy, value))
// goto error
// value.handle = NULL
// value.prefix = NULL
// }
// }
//
// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy,
// tag_directives_copy.start, tag_directives_copy.top,
// start_implicit, end_implicit, mark, mark)
//
// return 1
//
//error:
// STACK_DEL(&context, nodes)
// yaml_free(version_directive_copy)
// while (!STACK_EMPTY(&context, tag_directives_copy)) {
// value yaml_tag_directive_t = POP(&context, tag_directives_copy)
// yaml_free(value.handle)
// yaml_free(value.prefix)
// }
// STACK_DEL(&context, tag_directives_copy)
// yaml_free(value.handle)
// yaml_free(value.prefix)
//
// return 0
//}
//
///*
// * Destroy a document object.
// */
//
//YAML_DECLARE(void)
//yaml_document_delete(document *yaml_document_t)
//{
// struct {
// error yaml_error_type_t
// } context
// tag_directive *yaml_tag_directive_t
//
// context.error = YAML_NO_ERROR // Eliminate a compliler warning.
//
// assert(document) // Non-NULL document object is expected.
//
// while (!STACK_EMPTY(&context, document.nodes)) {
// node yaml_node_t = POP(&context, document.nodes)
// yaml_free(node.tag)
// switch (node.type) {
// case YAML_SCALAR_NODE:
// yaml_free(node.data.scalar.value)
// break
// case YAML_SEQUENCE_NODE:
// STACK_DEL(&context, node.data.sequence.items)
// break
// case YAML_MAPPING_NODE:
// STACK_DEL(&context, node.data.mapping.pairs)
// break
// default:
// assert(0) // Should not happen.
// }
// }
// STACK_DEL(&context, document.nodes)
//
// yaml_free(document.version_directive)
// for (tag_directive = document.tag_directives.start
// tag_directive != document.tag_directives.end
// tag_directive++) {
// yaml_free(tag_directive.handle)
// yaml_free(tag_directive.prefix)
// }
// yaml_free(document.tag_directives.start)
//
// memset(document, 0, sizeof(yaml_document_t))
//}
//
///**
// * Get a document node.
// */
//
//YAML_DECLARE(yaml_node_t *)
//yaml_document_get_node(document *yaml_document_t, index int)
//{
// assert(document) // Non-NULL document object is expected.
//
// if (index > 0 && document.nodes.start + index <= document.nodes.top) {
// return document.nodes.start + index - 1
// }
// return NULL
//}
//
///**
// * Get the root object.
// */
//
//YAML_DECLARE(yaml_node_t *)
//yaml_document_get_root_node(document *yaml_document_t)
//{
// assert(document) // Non-NULL document object is expected.
//
// if (document.nodes.top != document.nodes.start) {
// return document.nodes.start
// }
// return NULL
//}
//
///*
// * Add a scalar node to a document.
// */
//
//YAML_DECLARE(int)
//yaml_document_add_scalar(document *yaml_document_t,
// tag *yaml_char_t, value *yaml_char_t, length int,
// style yaml_scalar_style_t)
//{
// struct {
// error yaml_error_type_t
// } context
// mark yaml_mark_t = { 0, 0, 0 }
// tag_copy *yaml_char_t = NULL
// value_copy *yaml_char_t = NULL
// node yaml_node_t
//
// assert(document) // Non-NULL document object is expected.
// assert(value) // Non-NULL value is expected.
//
// if (!tag) {
// tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG
// }
//
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
// tag_copy = yaml_strdup(tag)
// if (!tag_copy) goto error
//
// if (length < 0) {
// length = strlen((char *)value)
// }
//
// if (!yaml_check_utf8(value, length)) goto error
// value_copy = yaml_malloc(length+1)
// if (!value_copy) goto error
// memcpy(value_copy, value, length)
// value_copy[length] = '\0'
//
// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark)
// if (!PUSH(&context, document.nodes, node)) goto error
//
// return document.nodes.top - document.nodes.start
//
//error:
// yaml_free(tag_copy)
// yaml_free(value_copy)
//
// return 0
//}
//
///*
// * Add a sequence node to a document.
// */
//
//YAML_DECLARE(int)
//yaml_document_add_sequence(document *yaml_document_t,
// tag *yaml_char_t, style yaml_sequence_style_t)
//{
// struct {
// error yaml_error_type_t
// } context
// mark yaml_mark_t = { 0, 0, 0 }
// tag_copy *yaml_char_t = NULL
// struct {
// start *yaml_node_item_t
// end *yaml_node_item_t
// top *yaml_node_item_t
// } items = { NULL, NULL, NULL }
// node yaml_node_t
//
// assert(document) // Non-NULL document object is expected.
//
// if (!tag) {
// tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG
// }
//
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
// tag_copy = yaml_strdup(tag)
// if (!tag_copy) goto error
//
// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error
//
// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end,
// style, mark, mark)
// if (!PUSH(&context, document.nodes, node)) goto error
//
// return document.nodes.top - document.nodes.start
//
//error:
// STACK_DEL(&context, items)
// yaml_free(tag_copy)
//
// return 0
//}
//
///*
// * Add a mapping node to a document.
// */
//
//YAML_DECLARE(int)
//yaml_document_add_mapping(document *yaml_document_t,
// tag *yaml_char_t, style yaml_mapping_style_t)
//{
// struct {
// error yaml_error_type_t
// } context
// mark yaml_mark_t = { 0, 0, 0 }
// tag_copy *yaml_char_t = NULL
// struct {
// start *yaml_node_pair_t
// end *yaml_node_pair_t
// top *yaml_node_pair_t
// } pairs = { NULL, NULL, NULL }
// node yaml_node_t
//
// assert(document) // Non-NULL document object is expected.
//
// if (!tag) {
// tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG
// }
//
// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
// tag_copy = yaml_strdup(tag)
// if (!tag_copy) goto error
//
// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error
//
// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end,
// style, mark, mark)
// if (!PUSH(&context, document.nodes, node)) goto error
//
// return document.nodes.top - document.nodes.start
//
//error:
// STACK_DEL(&context, pairs)
// yaml_free(tag_copy)
//
// return 0
//}
//
///*
// * Append an item to a sequence node.
// */
//
//YAML_DECLARE(int)
//yaml_document_append_sequence_item(document *yaml_document_t,
// sequence int, item int)
//{
// struct {
// error yaml_error_type_t
// } context
//
// assert(document) // Non-NULL document is required.
// assert(sequence > 0
// && document.nodes.start + sequence <= document.nodes.top)
// // Valid sequence id is required.
// assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE)
// // A sequence node is required.
// assert(item > 0 && document.nodes.start + item <= document.nodes.top)
// // Valid item id is required.
//
// if (!PUSH(&context,
// document.nodes.start[sequence-1].data.sequence.items, item))
// return 0
//
// return 1
//}
//
///*
// * Append a pair of a key and a value to a mapping node.
// */
//
//YAML_DECLARE(int)
//yaml_document_append_mapping_pair(document *yaml_document_t,
// mapping int, key int, value int)
//{
// struct {
// error yaml_error_type_t
// } context
//
// pair yaml_node_pair_t
//
// assert(document) // Non-NULL document is required.
// assert(mapping > 0
// && document.nodes.start + mapping <= document.nodes.top)
// // Valid mapping id is required.
// assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE)
// // A mapping node is required.
// assert(key > 0 && document.nodes.start + key <= document.nodes.top)
// // Valid key id is required.
// assert(value > 0 && document.nodes.start + value <= document.nodes.top)
// // Valid value id is required.
//
// pair.key = key
// pair.value = value
//
// if (!PUSH(&context,
// document.nodes.start[mapping-1].data.mapping.pairs, pair))
// return 0
//
// return 1
//}
//
//

View File

@@ -0,0 +1,538 @@
package yaml
import (
"reflect"
"strconv"
"time"
)
const (
documentNode = 1 << iota
mappingNode
sequenceNode
scalarNode
aliasNode
)
type node struct {
kind int
line, column int
tag string
value string
implicit bool
children []*node
anchors map[string]*node
}
// ----------------------------------------------------------------------------
// Parser, produces a node tree out of a libyaml event stream.
type parser struct {
parser yaml_parser_t
event yaml_event_t
doc *node
}
func newParser(b []byte) *parser {
p := parser{}
if !yaml_parser_initialize(&p.parser) {
panic("Failed to initialize YAML emitter")
}
if len(b) == 0 {
b = []byte{'\n'}
}
yaml_parser_set_input_string(&p.parser, b)
p.skip()
if p.event.typ != yaml_STREAM_START_EVENT {
panic("Expected stream start event, got " + strconv.Itoa(int(p.event.typ)))
}
p.skip()
return &p
}
func (p *parser) destroy() {
if p.event.typ != yaml_NO_EVENT {
yaml_event_delete(&p.event)
}
yaml_parser_delete(&p.parser)
}
func (p *parser) skip() {
if p.event.typ != yaml_NO_EVENT {
if p.event.typ == yaml_STREAM_END_EVENT {
panic("Attempted to go past the end of stream. Corrupted value?")
}
yaml_event_delete(&p.event)
}
if !yaml_parser_parse(&p.parser, &p.event) {
p.fail()
}
}
func (p *parser) fail() {
var where string
var line int
if p.parser.problem_mark.line != 0 {
line = p.parser.problem_mark.line
} else if p.parser.context_mark.line != 0 {
line = p.parser.context_mark.line
}
if line != 0 {
where = "line " + strconv.Itoa(line) + ": "
}
var msg string
if len(p.parser.problem) > 0 {
msg = p.parser.problem
} else {
msg = "Unknown problem parsing YAML content"
}
panic(where + msg)
}
func (p *parser) anchor(n *node, anchor []byte) {
if anchor != nil {
p.doc.anchors[string(anchor)] = n
}
}
func (p *parser) parse() *node {
switch p.event.typ {
case yaml_SCALAR_EVENT:
return p.scalar()
case yaml_ALIAS_EVENT:
return p.alias()
case yaml_MAPPING_START_EVENT:
return p.mapping()
case yaml_SEQUENCE_START_EVENT:
return p.sequence()
case yaml_DOCUMENT_START_EVENT:
return p.document()
case yaml_STREAM_END_EVENT:
// Happens when attempting to decode an empty buffer.
return nil
default:
panic("Attempted to parse unknown event: " +
strconv.Itoa(int(p.event.typ)))
}
panic("Unreachable")
}
func (p *parser) node(kind int) *node {
return &node{
kind: kind,
line: p.event.start_mark.line,
column: p.event.start_mark.column,
}
}
func (p *parser) document() *node {
n := p.node(documentNode)
n.anchors = make(map[string]*node)
p.doc = n
p.skip()
n.children = append(n.children, p.parse())
if p.event.typ != yaml_DOCUMENT_END_EVENT {
panic("Expected end of document event but got " +
strconv.Itoa(int(p.event.typ)))
}
p.skip()
return n
}
func (p *parser) alias() *node {
n := p.node(aliasNode)
n.value = string(p.event.anchor)
p.skip()
return n
}
func (p *parser) scalar() *node {
n := p.node(scalarNode)
n.value = string(p.event.value)
n.tag = string(p.event.tag)
n.implicit = p.event.implicit
p.anchor(n, p.event.anchor)
p.skip()
return n
}
func (p *parser) sequence() *node {
n := p.node(sequenceNode)
p.anchor(n, p.event.anchor)
p.skip()
for p.event.typ != yaml_SEQUENCE_END_EVENT {
n.children = append(n.children, p.parse())
}
p.skip()
return n
}
func (p *parser) mapping() *node {
n := p.node(mappingNode)
p.anchor(n, p.event.anchor)
p.skip()
for p.event.typ != yaml_MAPPING_END_EVENT {
n.children = append(n.children, p.parse(), p.parse())
}
p.skip()
return n
}
// ----------------------------------------------------------------------------
// Decoder, unmarshals a node into a provided value.
type decoder struct {
doc *node
aliases map[string]bool
}
func newDecoder() *decoder {
d := &decoder{}
d.aliases = make(map[string]bool)
return d
}
// d.setter deals with setters and pointer dereferencing and initialization.
//
// It's a slightly convoluted case to handle properly:
//
// - nil pointers should be initialized, unless being set to nil
// - we don't know at this point yet what's the value to SetYAML() with.
// - we can't separate pointer deref/init and setter checking, because
// a setter may be found while going down a pointer chain.
//
// Thus, here is how it takes care of it:
//
// - out is provided as a pointer, so that it can be replaced.
// - when looking at a non-setter ptr, *out=ptr.Elem(), unless tag=!!null
// - when a setter is found, *out=interface{}, and a set() function is
// returned to call SetYAML() with the value of *out once it's defined.
//
func (d *decoder) setter(tag string, out *reflect.Value, good *bool) (set func()) {
if (*out).Kind() != reflect.Ptr && (*out).CanAddr() {
setter, _ := (*out).Addr().Interface().(Setter)
if setter != nil {
var arg interface{}
*out = reflect.ValueOf(&arg).Elem()
return func() {
*good = setter.SetYAML(tag, arg)
}
}
}
again := true
for again {
again = false
setter, _ := (*out).Interface().(Setter)
if tag != "!!null" || setter != nil {
if pv := (*out); pv.Kind() == reflect.Ptr {
if pv.IsNil() {
*out = reflect.New(pv.Type().Elem()).Elem()
pv.Set((*out).Addr())
} else {
*out = pv.Elem()
}
setter, _ = pv.Interface().(Setter)
again = true
}
}
if setter != nil {
var arg interface{}
*out = reflect.ValueOf(&arg).Elem()
return func() {
*good = setter.SetYAML(tag, arg)
}
}
}
return nil
}
func (d *decoder) unmarshal(n *node, out reflect.Value) (good bool) {
switch n.kind {
case documentNode:
good = d.document(n, out)
case scalarNode:
good = d.scalar(n, out)
case aliasNode:
good = d.alias(n, out)
case mappingNode:
good = d.mapping(n, out)
case sequenceNode:
good = d.sequence(n, out)
default:
panic("Internal error: unknown node kind: " + strconv.Itoa(n.kind))
}
return
}
func (d *decoder) document(n *node, out reflect.Value) (good bool) {
if len(n.children) == 1 {
d.doc = n
d.unmarshal(n.children[0], out)
return true
}
return false
}
func (d *decoder) alias(n *node, out reflect.Value) (good bool) {
an, ok := d.doc.anchors[n.value]
if !ok {
panic("Unknown anchor '" + n.value + "' referenced")
}
if d.aliases[n.value] {
panic("Anchor '" + n.value + "' value contains itself")
}
d.aliases[n.value] = true
good = d.unmarshal(an, out)
delete(d.aliases, n.value)
return good
}
var durationType = reflect.TypeOf(time.Duration(0))
func (d *decoder) scalar(n *node, out reflect.Value) (good bool) {
var tag string
var resolved interface{}
if n.tag == "" && !n.implicit {
tag = "!!str"
resolved = n.value
} else {
tag, resolved = resolve(n.tag, n.value)
}
if set := d.setter(tag, &out, &good); set != nil {
defer set()
}
switch out.Kind() {
case reflect.String:
if resolved != nil {
out.SetString(n.value)
good = true
}
case reflect.Interface:
if resolved == nil {
out.Set(reflect.Zero(out.Type()))
} else {
out.Set(reflect.ValueOf(resolved))
}
good = true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch resolved := resolved.(type) {
case int:
if !out.OverflowInt(int64(resolved)) {
out.SetInt(int64(resolved))
good = true
}
case int64:
if !out.OverflowInt(resolved) {
out.SetInt(resolved)
good = true
}
case float64:
if resolved < 1<<63-1 && !out.OverflowInt(int64(resolved)) {
out.SetInt(int64(resolved))
good = true
}
case string:
if out.Type() == durationType {
d, err := time.ParseDuration(resolved)
if err == nil {
out.SetInt(int64(d))
good = true
}
}
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
switch resolved := resolved.(type) {
case int:
if resolved >= 0 {
out.SetUint(uint64(resolved))
good = true
}
case int64:
if resolved >= 0 {
out.SetUint(uint64(resolved))
good = true
}
case float64:
if resolved < 1<<64-1 && !out.OverflowUint(uint64(resolved)) {
out.SetUint(uint64(resolved))
good = true
}
}
case reflect.Bool:
switch resolved := resolved.(type) {
case bool:
out.SetBool(resolved)
good = true
}
case reflect.Float32, reflect.Float64:
switch resolved := resolved.(type) {
case int:
out.SetFloat(float64(resolved))
good = true
case int64:
out.SetFloat(float64(resolved))
good = true
case float64:
out.SetFloat(resolved)
good = true
}
case reflect.Ptr:
switch resolved.(type) {
case nil:
out.Set(reflect.Zero(out.Type()))
good = true
default:
if out.Type().Elem() == reflect.TypeOf(resolved) {
elem := reflect.New(out.Type().Elem())
elem.Elem().Set(reflect.ValueOf(resolved))
out.Set(elem)
good = true
}
}
}
return good
}
func settableValueOf(i interface{}) reflect.Value {
v := reflect.ValueOf(i)
sv := reflect.New(v.Type()).Elem()
sv.Set(v)
return sv
}
func (d *decoder) sequence(n *node, out reflect.Value) (good bool) {
if set := d.setter("!!seq", &out, &good); set != nil {
defer set()
}
var iface reflect.Value
if out.Kind() == reflect.Interface {
// No type hints. Will have to use a generic sequence.
iface = out
out = settableValueOf(make([]interface{}, 0))
}
if out.Kind() != reflect.Slice {
return false
}
et := out.Type().Elem()
l := len(n.children)
for i := 0; i < l; i++ {
e := reflect.New(et).Elem()
if ok := d.unmarshal(n.children[i], e); ok {
out.Set(reflect.Append(out, e))
}
}
if iface.IsValid() {
iface.Set(out)
}
return true
}
func (d *decoder) mapping(n *node, out reflect.Value) (good bool) {
if set := d.setter("!!map", &out, &good); set != nil {
defer set()
}
if out.Kind() == reflect.Struct {
return d.mappingStruct(n, out)
}
if out.Kind() == reflect.Interface {
// No type hints. Will have to use a generic map.
iface := out
out = settableValueOf(make(map[interface{}]interface{}))
iface.Set(out)
}
if out.Kind() != reflect.Map {
return false
}
outt := out.Type()
kt := outt.Key()
et := outt.Elem()
if out.IsNil() {
out.Set(reflect.MakeMap(outt))
}
l := len(n.children)
for i := 0; i < l; i += 2 {
if isMerge(n.children[i]) {
d.merge(n.children[i+1], out)
continue
}
k := reflect.New(kt).Elem()
if d.unmarshal(n.children[i], k) {
e := reflect.New(et).Elem()
if d.unmarshal(n.children[i+1], e) {
out.SetMapIndex(k, e)
}
}
}
return true
}
func (d *decoder) mappingStruct(n *node, out reflect.Value) (good bool) {
sinfo, err := getStructInfo(out.Type())
if err != nil {
panic(err)
}
name := settableValueOf("")
l := len(n.children)
for i := 0; i < l; i += 2 {
ni := n.children[i]
if isMerge(ni) {
d.merge(n.children[i+1], out)
continue
}
if !d.unmarshal(ni, name) {
continue
}
if info, ok := sinfo.FieldsMap[name.String()]; ok {
var field reflect.Value
if info.Inline == nil {
field = out.Field(info.Num)
} else {
field = out.FieldByIndex(info.Inline)
}
d.unmarshal(n.children[i+1], field)
}
}
return true
}
func (d *decoder) merge(n *node, out reflect.Value) {
const wantMap = "map merge requires map or sequence of maps as the value"
switch n.kind {
case mappingNode:
d.unmarshal(n, out)
case aliasNode:
an, ok := d.doc.anchors[n.value]
if ok && an.kind != mappingNode {
panic(wantMap)
}
d.unmarshal(n, out)
case sequenceNode:
// Step backwards as earlier nodes take precedence.
for i := len(n.children)-1; i >= 0; i-- {
ni := n.children[i]
if ni.kind == aliasNode {
an, ok := d.doc.anchors[ni.value]
if ok && an.kind != mappingNode {
panic(wantMap)
}
} else if ni.kind != mappingNode {
panic(wantMap)
}
d.unmarshal(ni, out)
}
default:
panic(wantMap)
}
}
func isMerge(n *node) bool {
return n.kind == scalarNode && n.value == "<<" && (n.implicit == true || n.tag == "!!merge" || n.tag == "tag:yaml.org,2002:merge")
}

View File

@@ -0,0 +1,648 @@
package yaml_test
import (
. "gopkg.in/check.v1"
"gopkg.in/yaml.v1"
"math"
"reflect"
"time"
)
var unmarshalIntTest = 123
var unmarshalTests = []struct {
data string
value interface{}
}{
{
"",
&struct{}{},
}, {
"{}", &struct{}{},
}, {
"v: hi",
map[string]string{"v": "hi"},
}, {
"v: hi", map[string]interface{}{"v": "hi"},
}, {
"v: true",
map[string]string{"v": "true"},
}, {
"v: true",
map[string]interface{}{"v": true},
}, {
"v: 10",
map[string]interface{}{"v": 10},
}, {
"v: 0b10",
map[string]interface{}{"v": 2},
}, {
"v: 0xA",
map[string]interface{}{"v": 10},
}, {
"v: 4294967296",
map[string]int64{"v": 4294967296},
}, {
"v: 0.1",
map[string]interface{}{"v": 0.1},
}, {
"v: .1",
map[string]interface{}{"v": 0.1},
}, {
"v: .Inf",
map[string]interface{}{"v": math.Inf(+1)},
}, {
"v: -.Inf",
map[string]interface{}{"v": math.Inf(-1)},
}, {
"v: -10",
map[string]interface{}{"v": -10},
}, {
"v: -.1",
map[string]interface{}{"v": -0.1},
},
// Simple values.
{
"123",
&unmarshalIntTest,
},
// Floats from spec
{
"canonical: 6.8523e+5",
map[string]interface{}{"canonical": 6.8523e+5},
}, {
"expo: 685.230_15e+03",
map[string]interface{}{"expo": 685.23015e+03},
}, {
"fixed: 685_230.15",
map[string]interface{}{"fixed": 685230.15},
}, {
"neginf: -.inf",
map[string]interface{}{"neginf": math.Inf(-1)},
}, {
"fixed: 685_230.15",
map[string]float64{"fixed": 685230.15},
},
//{"sexa: 190:20:30.15", map[string]interface{}{"sexa": 0}}, // Unsupported
//{"notanum: .NaN", map[string]interface{}{"notanum": math.NaN()}}, // Equality of NaN fails.
// Bools from spec
{
"canonical: y",
map[string]interface{}{"canonical": true},
}, {
"answer: NO",
map[string]interface{}{"answer": false},
}, {
"logical: True",
map[string]interface{}{"logical": true},
}, {
"option: on",
map[string]interface{}{"option": true},
}, {
"option: on",
map[string]bool{"option": true},
},
// Ints from spec
{
"canonical: 685230",
map[string]interface{}{"canonical": 685230},
}, {
"decimal: +685_230",
map[string]interface{}{"decimal": 685230},
}, {
"octal: 02472256",
map[string]interface{}{"octal": 685230},
}, {
"hexa: 0x_0A_74_AE",
map[string]interface{}{"hexa": 685230},
}, {
"bin: 0b1010_0111_0100_1010_1110",
map[string]interface{}{"bin": 685230},
}, {
"bin: -0b101010",
map[string]interface{}{"bin": -42},
}, {
"decimal: +685_230",
map[string]int{"decimal": 685230},
},
//{"sexa: 190:20:30", map[string]interface{}{"sexa": 0}}, // Unsupported
// Nulls from spec
{
"empty:",
map[string]interface{}{"empty": nil},
}, {
"canonical: ~",
map[string]interface{}{"canonical": nil},
}, {
"english: null",
map[string]interface{}{"english": nil},
}, {
"~: null key",
map[interface{}]string{nil: "null key"},
}, {
"empty:",
map[string]*bool{"empty": nil},
},
// Flow sequence
{
"seq: [A,B]",
map[string]interface{}{"seq": []interface{}{"A", "B"}},
}, {
"seq: [A,B,C,]",
map[string][]string{"seq": []string{"A", "B", "C"}},
}, {
"seq: [A,1,C]",
map[string][]string{"seq": []string{"A", "1", "C"}},
}, {
"seq: [A,1,C]",
map[string][]int{"seq": []int{1}},
}, {
"seq: [A,1,C]",
map[string]interface{}{"seq": []interface{}{"A", 1, "C"}},
},
// Block sequence
{
"seq:\n - A\n - B",
map[string]interface{}{"seq": []interface{}{"A", "B"}},
}, {
"seq:\n - A\n - B\n - C",
map[string][]string{"seq": []string{"A", "B", "C"}},
}, {
"seq:\n - A\n - 1\n - C",
map[string][]string{"seq": []string{"A", "1", "C"}},
}, {
"seq:\n - A\n - 1\n - C",
map[string][]int{"seq": []int{1}},
}, {
"seq:\n - A\n - 1\n - C",
map[string]interface{}{"seq": []interface{}{"A", 1, "C"}},
},
// Literal block scalar
{
"scalar: | # Comment\n\n literal\n\n \ttext\n\n",
map[string]string{"scalar": "\nliteral\n\n\ttext\n"},
},
// Folded block scalar
{
"scalar: > # Comment\n\n folded\n line\n \n next\n line\n * one\n * two\n\n last\n line\n\n",
map[string]string{"scalar": "\nfolded line\nnext line\n * one\n * two\n\nlast line\n"},
},
// Map inside interface with no type hints.
{
"a: {b: c}",
map[string]interface{}{"a": map[interface{}]interface{}{"b": "c"}},
},
// Structs and type conversions.
{
"hello: world",
&struct{ Hello string }{"world"},
}, {
"a: {b: c}",
&struct{ A struct{ B string } }{struct{ B string }{"c"}},
}, {
"a: {b: c}",
&struct{ A *struct{ B string } }{&struct{ B string }{"c"}},
}, {
"a: {b: c}",
&struct{ A map[string]string }{map[string]string{"b": "c"}},
}, {
"a: {b: c}",
&struct{ A *map[string]string }{&map[string]string{"b": "c"}},
}, {
"a:",
&struct{ A map[string]string }{},
}, {
"a: 1",
&struct{ A int }{1},
}, {
"a: 1",
&struct{ A float64 }{1},
}, {
"a: 1.0",
&struct{ A int }{1},
}, {
"a: 1.0",
&struct{ A uint }{1},
}, {
"a: [1, 2]",
&struct{ A []int }{[]int{1, 2}},
}, {
"a: 1",
&struct{ B int }{0},
}, {
"a: 1",
&struct {
B int "a"
}{1},
}, {
"a: y",
&struct{ A bool }{true},
},
// Some cross type conversions
{
"v: 42",
map[string]uint{"v": 42},
}, {
"v: -42",
map[string]uint{},
}, {
"v: 4294967296",
map[string]uint64{"v": 4294967296},
}, {
"v: -4294967296",
map[string]uint64{},
},
// Overflow cases.
{
"v: 4294967297",
map[string]int32{},
}, {
"v: 128",
map[string]int8{},
},
// Quoted values.
{
"'1': '\"2\"'",
map[interface{}]interface{}{"1": "\"2\""},
}, {
"v:\n- A\n- 'B\n\n C'\n",
map[string][]string{"v": []string{"A", "B\nC"}},
},
// Explicit tags.
{
"v: !!float '1.1'",
map[string]interface{}{"v": 1.1},
}, {
"v: !!null ''",
map[string]interface{}{"v": nil},
}, {
"%TAG !y! tag:yaml.org,2002:\n---\nv: !y!int '1'",
map[string]interface{}{"v": 1},
},
// Anchors and aliases.
{
"a: &x 1\nb: &y 2\nc: *x\nd: *y\n",
&struct{ A, B, C, D int }{1, 2, 1, 2},
}, {
"a: &a {c: 1}\nb: *a",
&struct {
A, B struct {
C int
}
}{struct{ C int }{1}, struct{ C int }{1}},
}, {
"a: &a [1, 2]\nb: *a",
&struct{ B []int }{[]int{1, 2}},
},
// Bug #1133337
{
"foo: ''",
map[string]*string{"foo": new(string)},
}, {
"foo: null",
map[string]string{},
},
// Ignored field
{
"a: 1\nb: 2\n",
&struct {
A int
B int "-"
}{1, 0},
},
// Bug #1191981
{
"" +
"%YAML 1.1\n" +
"--- !!str\n" +
`"Generic line break (no glyph)\n\` + "\n" +
` Generic line break (glyphed)\n\` + "\n" +
` Line separator\u2028\` + "\n" +
` Paragraph separator\u2029"` + "\n",
"" +
"Generic line break (no glyph)\n" +
"Generic line break (glyphed)\n" +
"Line separator\u2028Paragraph separator\u2029",
},
// Struct inlining
{
"a: 1\nb: 2\nc: 3\n",
&struct {
A int
C inlineB `yaml:",inline"`
}{1, inlineB{2, inlineC{3}}},
},
// bug 1243827
{
"a: -b_c",
map[string]interface{}{"a": "-b_c"},
},
{
"a: +b_c",
map[string]interface{}{"a": "+b_c"},
},
{
"a: 50cent_of_dollar",
map[string]interface{}{"a": "50cent_of_dollar"},
},
// Duration
{
"a: 3s",
map[string]time.Duration{"a": 3 * time.Second},
},
}
type inlineB struct {
B int
inlineC `yaml:",inline"`
}
type inlineC struct {
C int
}
func (s *S) TestUnmarshal(c *C) {
for i, item := range unmarshalTests {
t := reflect.ValueOf(item.value).Type()
var value interface{}
switch t.Kind() {
case reflect.Map:
value = reflect.MakeMap(t).Interface()
case reflect.String:
t := reflect.ValueOf(item.value).Type()
v := reflect.New(t)
value = v.Interface()
default:
pt := reflect.ValueOf(item.value).Type()
pv := reflect.New(pt.Elem())
value = pv.Interface()
}
err := yaml.Unmarshal([]byte(item.data), value)
c.Assert(err, IsNil, Commentf("Item #%d", i))
if t.Kind() == reflect.String {
c.Assert(*value.(*string), Equals, item.value, Commentf("Item #%d", i))
} else {
c.Assert(value, DeepEquals, item.value, Commentf("Item #%d", i))
}
}
}
func (s *S) TestUnmarshalNaN(c *C) {
value := map[string]interface{}{}
err := yaml.Unmarshal([]byte("notanum: .NaN"), &value)
c.Assert(err, IsNil)
c.Assert(math.IsNaN(value["notanum"].(float64)), Equals, true)
}
var unmarshalErrorTests = []struct {
data, error string
}{
{"v: !!float 'error'", "YAML error: Can't decode !!str 'error' as a !!float"},
{"v: [A,", "YAML error: line 1: did not find expected node content"},
{"v:\n- [A,", "YAML error: line 2: did not find expected node content"},
{"a: *b\n", "YAML error: Unknown anchor 'b' referenced"},
{"a: &a\n b: *a\n", "YAML error: Anchor 'a' value contains itself"},
{"value: -", "YAML error: block sequence entries are not allowed in this context"},
}
func (s *S) TestUnmarshalErrors(c *C) {
for _, item := range unmarshalErrorTests {
var value interface{}
err := yaml.Unmarshal([]byte(item.data), &value)
c.Assert(err, ErrorMatches, item.error, Commentf("Partial unmarshal: %#v", value))
}
}
var setterTests = []struct {
data, tag string
value interface{}
}{
{"_: {hi: there}", "!!map", map[interface{}]interface{}{"hi": "there"}},
{"_: [1,A]", "!!seq", []interface{}{1, "A"}},
{"_: 10", "!!int", 10},
{"_: null", "!!null", nil},
{`_: BAR!`, "!!str", "BAR!"},
{`_: "BAR!"`, "!!str", "BAR!"},
{"_: !!foo 'BAR!'", "!!foo", "BAR!"},
}
var setterResult = map[int]bool{}
type typeWithSetter struct {
tag string
value interface{}
}
func (o *typeWithSetter) SetYAML(tag string, value interface{}) (ok bool) {
o.tag = tag
o.value = value
if i, ok := value.(int); ok {
if result, ok := setterResult[i]; ok {
return result
}
}
return true
}
type setterPointerType struct {
Field *typeWithSetter "_"
}
type setterValueType struct {
Field typeWithSetter "_"
}
func (s *S) TestUnmarshalWithPointerSetter(c *C) {
for _, item := range setterTests {
obj := &setterPointerType{}
err := yaml.Unmarshal([]byte(item.data), obj)
c.Assert(err, IsNil)
c.Assert(obj.Field, NotNil, Commentf("Pointer not initialized (%#v)", item.value))
c.Assert(obj.Field.tag, Equals, item.tag)
c.Assert(obj.Field.value, DeepEquals, item.value)
}
}
func (s *S) TestUnmarshalWithValueSetter(c *C) {
for _, item := range setterTests {
obj := &setterValueType{}
err := yaml.Unmarshal([]byte(item.data), obj)
c.Assert(err, IsNil)
c.Assert(obj.Field, NotNil, Commentf("Pointer not initialized (%#v)", item.value))
c.Assert(obj.Field.tag, Equals, item.tag)
c.Assert(obj.Field.value, DeepEquals, item.value)
}
}
func (s *S) TestUnmarshalWholeDocumentWithSetter(c *C) {
obj := &typeWithSetter{}
err := yaml.Unmarshal([]byte(setterTests[0].data), obj)
c.Assert(err, IsNil)
c.Assert(obj.tag, Equals, setterTests[0].tag)
value, ok := obj.value.(map[interface{}]interface{})
c.Assert(ok, Equals, true)
c.Assert(value["_"], DeepEquals, setterTests[0].value)
}
func (s *S) TestUnmarshalWithFalseSetterIgnoresValue(c *C) {
setterResult[2] = false
setterResult[4] = false
defer func() {
delete(setterResult, 2)
delete(setterResult, 4)
}()
m := map[string]*typeWithSetter{}
data := `{abc: 1, def: 2, ghi: 3, jkl: 4}`
err := yaml.Unmarshal([]byte(data), m)
c.Assert(err, IsNil)
c.Assert(m["abc"], NotNil)
c.Assert(m["def"], IsNil)
c.Assert(m["ghi"], NotNil)
c.Assert(m["jkl"], IsNil)
c.Assert(m["abc"].value, Equals, 1)
c.Assert(m["ghi"].value, Equals, 3)
}
// From http://yaml.org/type/merge.html
var mergeTests = `
anchors:
- &CENTER { "x": 1, "y": 2 }
- &LEFT { "x": 0, "y": 2 }
- &BIG { "r": 10 }
- &SMALL { "r": 1 }
# All the following maps are equal:
plain:
# Explicit keys
"x": 1
"y": 2
"r": 10
label: center/big
mergeOne:
# Merge one map
<< : *CENTER
"r": 10
label: center/big
mergeMultiple:
# Merge multiple maps
<< : [ *CENTER, *BIG ]
label: center/big
override:
# Override
<< : [ *BIG, *LEFT, *SMALL ]
"x": 1
label: center/big
shortTag:
# Explicit short merge tag
!!merge "<<" : [ *CENTER, *BIG ]
label: center/big
longTag:
# Explicit merge long tag
!<tag:yaml.org,2002:merge> "<<" : [ *CENTER, *BIG ]
label: center/big
inlineMap:
# Inlined map
<< : {"x": 1, "y": 2, "r": 10}
label: center/big
inlineSequenceMap:
# Inlined map in sequence
<< : [ *CENTER, {"r": 10} ]
label: center/big
`
func (s *S) TestMerge(c *C) {
var want = map[interface{}]interface{}{
"x": 1,
"y": 2,
"r": 10,
"label": "center/big",
}
var m map[string]interface{}
err := yaml.Unmarshal([]byte(mergeTests), &m)
c.Assert(err, IsNil)
for name, test := range m {
if name == "anchors" {
continue
}
c.Assert(test, DeepEquals, want, Commentf("test %q failed", name))
}
}
func (s *S) TestMergeStruct(c *C) {
type Data struct {
X, Y, R int
Label string
}
want := Data{1, 2, 10, "center/big"}
var m map[string]Data
err := yaml.Unmarshal([]byte(mergeTests), &m)
c.Assert(err, IsNil)
for name, test := range m {
if name == "anchors" {
continue
}
c.Assert(test, Equals, want, Commentf("test %q failed", name))
}
}
//var data []byte
//func init() {
// var err error
// data, err = ioutil.ReadFile("/tmp/file.yaml")
// if err != nil {
// panic(err)
// }
//}
//
//func (s *S) BenchmarkUnmarshal(c *C) {
// var err error
// for i := 0; i < c.N; i++ {
// var v map[string]interface{}
// err = yaml.Unmarshal(data, &v)
// }
// if err != nil {
// panic(err)
// }
//}
//
//func (s *S) BenchmarkMarshal(c *C) {
// var v map[string]interface{}
// yaml.Unmarshal(data, &v)
// c.ResetTimer()
// for i := 0; i < c.N; i++ {
// yaml.Marshal(&v)
// }
//}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,226 @@
package yaml
import (
"reflect"
"sort"
"strconv"
"time"
)
type encoder struct {
emitter yaml_emitter_t
event yaml_event_t
out []byte
flow bool
}
func newEncoder() (e *encoder) {
e = &encoder{}
e.must(yaml_emitter_initialize(&e.emitter))
yaml_emitter_set_output_string(&e.emitter, &e.out)
e.must(yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING))
e.emit()
e.must(yaml_document_start_event_initialize(&e.event, nil, nil, true))
e.emit()
return e
}
func (e *encoder) finish() {
e.must(yaml_document_end_event_initialize(&e.event, true))
e.emit()
e.emitter.open_ended = false
e.must(yaml_stream_end_event_initialize(&e.event))
e.emit()
}
func (e *encoder) destroy() {
yaml_emitter_delete(&e.emitter)
}
func (e *encoder) emit() {
// This will internally delete the e.event value.
if !yaml_emitter_emit(&e.emitter, &e.event) && e.event.typ != yaml_DOCUMENT_END_EVENT && e.event.typ != yaml_STREAM_END_EVENT {
e.must(false)
}
}
func (e *encoder) must(ok bool) {
if !ok {
msg := e.emitter.problem
if msg == "" {
msg = "Unknown problem generating YAML content"
}
panic(msg)
}
}
func (e *encoder) marshal(tag string, in reflect.Value) {
var value interface{}
if getter, ok := in.Interface().(Getter); ok {
tag, value = getter.GetYAML()
if value == nil {
e.nilv()
return
}
in = reflect.ValueOf(value)
}
switch in.Kind() {
case reflect.Interface:
if in.IsNil() {
e.nilv()
} else {
e.marshal(tag, in.Elem())
}
case reflect.Map:
e.mapv(tag, in)
case reflect.Ptr:
if in.IsNil() {
e.nilv()
} else {
e.marshal(tag, in.Elem())
}
case reflect.Struct:
e.structv(tag, in)
case reflect.Slice:
e.slicev(tag, in)
case reflect.String:
e.stringv(tag, in)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if in.Type() == durationType {
e.stringv(tag, reflect.ValueOf(in.Interface().(time.Duration).String()))
} else {
e.intv(tag, in)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
e.uintv(tag, in)
case reflect.Float32, reflect.Float64:
e.floatv(tag, in)
case reflect.Bool:
e.boolv(tag, in)
default:
panic("Can't marshal type yet: " + in.Type().String())
}
}
func (e *encoder) mapv(tag string, in reflect.Value) {
e.mappingv(tag, func() {
keys := keyList(in.MapKeys())
sort.Sort(keys)
for _, k := range keys {
e.marshal("", k)
e.marshal("", in.MapIndex(k))
}
})
}
func (e *encoder) structv(tag string, in reflect.Value) {
sinfo, err := getStructInfo(in.Type())
if err != nil {
panic(err)
}
e.mappingv(tag, func() {
for _, info := range sinfo.FieldsList {
var value reflect.Value
if info.Inline == nil {
value = in.Field(info.Num)
} else {
value = in.FieldByIndex(info.Inline)
}
if info.OmitEmpty && isZero(value) {
continue
}
e.marshal("", reflect.ValueOf(info.Key))
e.flow = info.Flow
e.marshal("", value)
}
})
}
func (e *encoder) mappingv(tag string, f func()) {
implicit := tag == ""
style := yaml_BLOCK_MAPPING_STYLE
if e.flow {
e.flow = false
style = yaml_FLOW_MAPPING_STYLE
}
e.must(yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style))
e.emit()
f()
e.must(yaml_mapping_end_event_initialize(&e.event))
e.emit()
}
func (e *encoder) slicev(tag string, in reflect.Value) {
implicit := tag == ""
style := yaml_BLOCK_SEQUENCE_STYLE
if e.flow {
e.flow = false
style = yaml_FLOW_SEQUENCE_STYLE
}
e.must(yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style))
e.emit()
n := in.Len()
for i := 0; i < n; i++ {
e.marshal("", in.Index(i))
}
e.must(yaml_sequence_end_event_initialize(&e.event))
e.emit()
}
func (e *encoder) stringv(tag string, in reflect.Value) {
var style yaml_scalar_style_t
s := in.String()
if rtag, _ := resolve("", s); rtag != "!!str" {
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
} else {
style = yaml_PLAIN_SCALAR_STYLE
}
e.emitScalar(s, "", tag, style)
}
func (e *encoder) boolv(tag string, in reflect.Value) {
var s string
if in.Bool() {
s = "true"
} else {
s = "false"
}
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE)
}
func (e *encoder) intv(tag string, in reflect.Value) {
s := strconv.FormatInt(in.Int(), 10)
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE)
}
func (e *encoder) uintv(tag string, in reflect.Value) {
s := strconv.FormatUint(in.Uint(), 10)
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE)
}
func (e *encoder) floatv(tag string, in reflect.Value) {
// FIXME: Handle 64 bits here.
s := strconv.FormatFloat(float64(in.Float()), 'g', -1, 32)
switch s {
case "+Inf":
s = ".inf"
case "-Inf":
s = "-.inf"
case "NaN":
s = ".nan"
}
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE)
}
func (e *encoder) nilv() {
e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE)
}
func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t) {
implicit := tag == ""
if !implicit {
style = yaml_PLAIN_SCALAR_STYLE
}
e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style))
e.emit()
}

View File

@@ -0,0 +1,386 @@
package yaml_test
import (
"fmt"
"gopkg.in/yaml.v1"
. "gopkg.in/check.v1"
"math"
"strconv"
"strings"
"time"
)
var marshalIntTest = 123
var marshalTests = []struct {
value interface{}
data string
}{
{
&struct{}{},
"{}\n",
}, {
map[string]string{"v": "hi"},
"v: hi\n",
}, {
map[string]interface{}{"v": "hi"},
"v: hi\n",
}, {
map[string]string{"v": "true"},
"v: \"true\"\n",
}, {
map[string]string{"v": "false"},
"v: \"false\"\n",
}, {
map[string]interface{}{"v": true},
"v: true\n",
}, {
map[string]interface{}{"v": false},
"v: false\n",
}, {
map[string]interface{}{"v": 10},
"v: 10\n",
}, {
map[string]interface{}{"v": -10},
"v: -10\n",
}, {
map[string]uint{"v": 42},
"v: 42\n",
}, {
map[string]interface{}{"v": int64(4294967296)},
"v: 4294967296\n",
}, {
map[string]int64{"v": int64(4294967296)},
"v: 4294967296\n",
}, {
map[string]uint64{"v": 4294967296},
"v: 4294967296\n",
}, {
map[string]interface{}{"v": "10"},
"v: \"10\"\n",
}, {
map[string]interface{}{"v": 0.1},
"v: 0.1\n",
}, {
map[string]interface{}{"v": float64(0.1)},
"v: 0.1\n",
}, {
map[string]interface{}{"v": -0.1},
"v: -0.1\n",
}, {
map[string]interface{}{"v": math.Inf(+1)},
"v: .inf\n",
}, {
map[string]interface{}{"v": math.Inf(-1)},
"v: -.inf\n",
}, {
map[string]interface{}{"v": math.NaN()},
"v: .nan\n",
}, {
map[string]interface{}{"v": nil},
"v: null\n",
}, {
map[string]interface{}{"v": ""},
"v: \"\"\n",
}, {
map[string][]string{"v": []string{"A", "B"}},
"v:\n- A\n- B\n",
}, {
map[string][]string{"v": []string{"A", "B\nC"}},
"v:\n- A\n- 'B\n\n C'\n",
}, {
map[string][]interface{}{"v": []interface{}{"A", 1, map[string][]int{"B": []int{2, 3}}}},
"v:\n- A\n- 1\n- B:\n - 2\n - 3\n",
}, {
map[string]interface{}{"a": map[interface{}]interface{}{"b": "c"}},
"a:\n b: c\n",
}, {
map[string]interface{}{"a": "-"},
"a: '-'\n",
},
// Simple values.
{
&marshalIntTest,
"123\n",
},
// Structures
{
&struct{ Hello string }{"world"},
"hello: world\n",
}, {
&struct {
A struct {
B string
}
}{struct{ B string }{"c"}},
"a:\n b: c\n",
}, {
&struct {
A *struct {
B string
}
}{&struct{ B string }{"c"}},
"a:\n b: c\n",
}, {
&struct {
A *struct {
B string
}
}{},
"a: null\n",
}, {
&struct{ A int }{1},
"a: 1\n",
}, {
&struct{ A []int }{[]int{1, 2}},
"a:\n- 1\n- 2\n",
}, {
&struct {
B int "a"
}{1},
"a: 1\n",
}, {
&struct{ A bool }{true},
"a: true\n",
},
// Conditional flag
{
&struct {
A int "a,omitempty"
B int "b,omitempty"
}{1, 0},
"a: 1\n",
}, {
&struct {
A int "a,omitempty"
B int "b,omitempty"
}{0, 0},
"{}\n",
}, {
&struct {
A *struct{ X int } "a,omitempty"
B int "b,omitempty"
}{nil, 0},
"{}\n",
},
// Flow flag
{
&struct {
A []int "a,flow"
}{[]int{1, 2}},
"a: [1, 2]\n",
}, {
&struct {
A map[string]string "a,flow"
}{map[string]string{"b": "c", "d": "e"}},
"a: {b: c, d: e}\n",
}, {
&struct {
A struct {
B, D string
} "a,flow"
}{struct{ B, D string }{"c", "e"}},
"a: {b: c, d: e}\n",
},
// Unexported field
{
&struct {
u int
A int
}{0, 1},
"a: 1\n",
},
// Ignored field
{
&struct {
A int
B int "-"
}{1, 2},
"a: 1\n",
},
// Struct inlining
{
&struct {
A int
C inlineB `yaml:",inline"`
}{1, inlineB{2, inlineC{3}}},
"a: 1\nb: 2\nc: 3\n",
},
// Duration
{
map[string]time.Duration{"a": 3 * time.Second},
"a: 3s\n",
},
}
func (s *S) TestMarshal(c *C) {
for _, item := range marshalTests {
data, err := yaml.Marshal(item.value)
c.Assert(err, IsNil)
c.Assert(string(data), Equals, item.data)
}
}
var marshalErrorTests = []struct {
value interface{}
error string
}{
{
&struct {
B int
inlineB ",inline"
}{1, inlineB{2, inlineC{3}}},
`Duplicated key 'b' in struct struct \{ B int; .*`,
},
}
func (s *S) TestMarshalErrors(c *C) {
for _, item := range marshalErrorTests {
_, err := yaml.Marshal(item.value)
c.Assert(err, ErrorMatches, item.error)
}
}
var marshalTaggedIfaceTest interface{} = &struct{ A string }{"B"}
var getterTests = []struct {
data, tag string
value interface{}
}{
{"_:\n hi: there\n", "", map[interface{}]interface{}{"hi": "there"}},
{"_:\n- 1\n- A\n", "", []interface{}{1, "A"}},
{"_: 10\n", "", 10},
{"_: null\n", "", nil},
{"_: !foo BAR!\n", "!foo", "BAR!"},
{"_: !foo 1\n", "!foo", "1"},
{"_: !foo '\"1\"'\n", "!foo", "\"1\""},
{"_: !foo 1.1\n", "!foo", 1.1},
{"_: !foo 1\n", "!foo", 1},
{"_: !foo 1\n", "!foo", uint(1)},
{"_: !foo true\n", "!foo", true},
{"_: !foo\n- A\n- B\n", "!foo", []string{"A", "B"}},
{"_: !foo\n A: B\n", "!foo", map[string]string{"A": "B"}},
{"_: !foo\n a: B\n", "!foo", &marshalTaggedIfaceTest},
}
func (s *S) TestMarshalTypeCache(c *C) {
var data []byte
var err error
func() {
type T struct{ A int }
data, err = yaml.Marshal(&T{})
c.Assert(err, IsNil)
}()
func() {
type T struct{ B int }
data, err = yaml.Marshal(&T{})
c.Assert(err, IsNil)
}()
c.Assert(string(data), Equals, "b: 0\n")
}
type typeWithGetter struct {
tag string
value interface{}
}
func (o typeWithGetter) GetYAML() (tag string, value interface{}) {
return o.tag, o.value
}
type typeWithGetterField struct {
Field typeWithGetter "_"
}
func (s *S) TestMashalWithGetter(c *C) {
for _, item := range getterTests {
obj := &typeWithGetterField{}
obj.Field.tag = item.tag
obj.Field.value = item.value
data, err := yaml.Marshal(obj)
c.Assert(err, IsNil)
c.Assert(string(data), Equals, string(item.data))
}
}
func (s *S) TestUnmarshalWholeDocumentWithGetter(c *C) {
obj := &typeWithGetter{}
obj.tag = ""
obj.value = map[string]string{"hello": "world!"}
data, err := yaml.Marshal(obj)
c.Assert(err, IsNil)
c.Assert(string(data), Equals, "hello: world!\n")
}
func (s *S) TestSortedOutput(c *C) {
order := []interface{}{
false,
true,
1,
uint(1),
1.0,
1.1,
1.2,
2,
uint(2),
2.0,
2.1,
"",
".1",
".2",
".a",
"1",
"2",
"a!10",
"a/2",
"a/10",
"a~10",
"ab/1",
"b/1",
"b/01",
"b/2",
"b/02",
"b/3",
"b/03",
"b1",
"b01",
"b3",
"c2.10",
"c10.2",
"d1",
"d12",
"d12a",
}
m := make(map[interface{}]int)
for _, k := range order {
m[k] = 1
}
data, err := yaml.Marshal(m)
c.Assert(err, IsNil)
out := "\n" + string(data)
last := 0
for i, k := range order {
repr := fmt.Sprint(k)
if s, ok := k.(string); ok {
if _, err = strconv.ParseFloat(repr, 32); s == "" || err == nil {
repr = `"` + repr + `"`
}
}
index := strings.Index(out, "\n"+repr+":")
if index == -1 {
c.Fatalf("%#v is not in the output: %#v", k, out)
}
if index < last {
c.Fatalf("%#v was generated before %#v: %q", k, order[i-1], out)
}
last = index
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,391 @@
package yaml
import (
"io"
)
// Set the reader error and return 0.
func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, offset int, value int) bool {
parser.error = yaml_READER_ERROR
parser.problem = problem
parser.problem_offset = offset
parser.problem_value = value
return false
}
// Byte order marks.
const (
bom_UTF8 = "\xef\xbb\xbf"
bom_UTF16LE = "\xff\xfe"
bom_UTF16BE = "\xfe\xff"
)
// Determine the input stream encoding by checking the BOM symbol. If no BOM is
// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure.
func yaml_parser_determine_encoding(parser *yaml_parser_t) bool {
// Ensure that we had enough bytes in the raw buffer.
for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 {
if !yaml_parser_update_raw_buffer(parser) {
return false
}
}
// Determine the encoding.
buf := parser.raw_buffer
pos := parser.raw_buffer_pos
avail := len(buf) - pos
if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] {
parser.encoding = yaml_UTF16LE_ENCODING
parser.raw_buffer_pos += 2
parser.offset += 2
} else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] {
parser.encoding = yaml_UTF16BE_ENCODING
parser.raw_buffer_pos += 2
parser.offset += 2
} else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] {
parser.encoding = yaml_UTF8_ENCODING
parser.raw_buffer_pos += 3
parser.offset += 3
} else {
parser.encoding = yaml_UTF8_ENCODING
}
return true
}
// Update the raw buffer.
func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool {
size_read := 0
// Return if the raw buffer is full.
if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) {
return true
}
// Return on EOF.
if parser.eof {
return true
}
// Move the remaining bytes in the raw buffer to the beginning.
if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) {
copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:])
}
parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos]
parser.raw_buffer_pos = 0
// Call the read handler to fill the buffer.
size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)])
parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read]
if err == io.EOF {
parser.eof = true
} else if err != nil {
return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), parser.offset, -1)
}
return true
}
// Ensure that the buffer contains at least `length` characters.
// Return true on success, false on failure.
//
// The length is supposed to be significantly less that the buffer size.
func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool {
if parser.read_handler == nil {
panic("read handler must be set")
}
// If the EOF flag is set and the raw buffer is empty, do nothing.
if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) {
return true
}
// Return if the buffer contains enough characters.
if parser.unread >= length {
return true
}
// Determine the input encoding if it is not known yet.
if parser.encoding == yaml_ANY_ENCODING {
if !yaml_parser_determine_encoding(parser) {
return false
}
}
// Move the unread characters to the beginning of the buffer.
buffer_len := len(parser.buffer)
if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len {
copy(parser.buffer, parser.buffer[parser.buffer_pos:])
buffer_len -= parser.buffer_pos
parser.buffer_pos = 0
} else if parser.buffer_pos == buffer_len {
buffer_len = 0
parser.buffer_pos = 0
}
// Open the whole buffer for writing, and cut it before returning.
parser.buffer = parser.buffer[:cap(parser.buffer)]
// Fill the buffer until it has enough characters.
first := true
for parser.unread < length {
// Fill the raw buffer if necessary.
if !first || parser.raw_buffer_pos == len(parser.raw_buffer) {
if !yaml_parser_update_raw_buffer(parser) {
parser.buffer = parser.buffer[:buffer_len]
return false
}
}
first = false
// Decode the raw buffer.
inner:
for parser.raw_buffer_pos != len(parser.raw_buffer) {
var value rune
var width int
raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos
// Decode the next character.
switch parser.encoding {
case yaml_UTF8_ENCODING:
// Decode a UTF-8 character. Check RFC 3629
// (http://www.ietf.org/rfc/rfc3629.txt) for more details.
//
// The following table (taken from the RFC) is used for
// decoding.
//
// Char. number range | UTF-8 octet sequence
// (hexadecimal) | (binary)
// --------------------+------------------------------------
// 0000 0000-0000 007F | 0xxxxxxx
// 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
// 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
// 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
//
// Additionally, the characters in the range 0xD800-0xDFFF
// are prohibited as they are reserved for use with UTF-16
// surrogate pairs.
// Determine the length of the UTF-8 sequence.
octet := parser.raw_buffer[parser.raw_buffer_pos]
switch {
case octet&0x80 == 0x00:
width = 1
case octet&0xE0 == 0xC0:
width = 2
case octet&0xF0 == 0xE0:
width = 3
case octet&0xF8 == 0xF0:
width = 4
default:
// The leading octet is invalid.
return yaml_parser_set_reader_error(parser,
"invalid leading UTF-8 octet",
parser.offset, int(octet))
}
// Check if the raw buffer contains an incomplete character.
if width > raw_unread {
if parser.eof {
return yaml_parser_set_reader_error(parser,
"incomplete UTF-8 octet sequence",
parser.offset, -1)
}
break inner
}
// Decode the leading octet.
switch {
case octet&0x80 == 0x00:
value = rune(octet & 0x7F)
case octet&0xE0 == 0xC0:
value = rune(octet & 0x1F)
case octet&0xF0 == 0xE0:
value = rune(octet & 0x0F)
case octet&0xF8 == 0xF0:
value = rune(octet & 0x07)
default:
value = 0
}
// Check and decode the trailing octets.
for k := 1; k < width; k++ {
octet = parser.raw_buffer[parser.raw_buffer_pos+k]
// Check if the octet is valid.
if (octet & 0xC0) != 0x80 {
return yaml_parser_set_reader_error(parser,
"invalid trailing UTF-8 octet",
parser.offset+k, int(octet))
}
// Decode the octet.
value = (value << 6) + rune(octet&0x3F)
}
// Check the length of the sequence against the value.
switch {
case width == 1:
case width == 2 && value >= 0x80:
case width == 3 && value >= 0x800:
case width == 4 && value >= 0x10000:
default:
return yaml_parser_set_reader_error(parser,
"invalid length of a UTF-8 sequence",
parser.offset, -1)
}
// Check the range of the value.
if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF {
return yaml_parser_set_reader_error(parser,
"invalid Unicode character",
parser.offset, int(value))
}
case yaml_UTF16LE_ENCODING, yaml_UTF16BE_ENCODING:
var low, high int
if parser.encoding == yaml_UTF16LE_ENCODING {
low, high = 0, 1
} else {
high, low = 1, 0
}
// The UTF-16 encoding is not as simple as one might
// naively think. Check RFC 2781
// (http://www.ietf.org/rfc/rfc2781.txt).
//
// Normally, two subsequent bytes describe a Unicode
// character. However a special technique (called a
// surrogate pair) is used for specifying character
// values larger than 0xFFFF.
//
// A surrogate pair consists of two pseudo-characters:
// high surrogate area (0xD800-0xDBFF)
// low surrogate area (0xDC00-0xDFFF)
//
// The following formulas are used for decoding
// and encoding characters using surrogate pairs:
//
// U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF)
// U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF)
// W1 = 110110yyyyyyyyyy
// W2 = 110111xxxxxxxxxx
//
// where U is the character value, W1 is the high surrogate
// area, W2 is the low surrogate area.
// Check for incomplete UTF-16 character.
if raw_unread < 2 {
if parser.eof {
return yaml_parser_set_reader_error(parser,
"incomplete UTF-16 character",
parser.offset, -1)
}
break inner
}
// Get the character.
value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) +
(rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8)
// Check for unexpected low surrogate area.
if value&0xFC00 == 0xDC00 {
return yaml_parser_set_reader_error(parser,
"unexpected low surrogate area",
parser.offset, int(value))
}
// Check for a high surrogate area.
if value&0xFC00 == 0xD800 {
width = 4
// Check for incomplete surrogate pair.
if raw_unread < 4 {
if parser.eof {
return yaml_parser_set_reader_error(parser,
"incomplete UTF-16 surrogate pair",
parser.offset, -1)
}
break inner
}
// Get the next character.
value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) +
(rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8)
// Check for a low surrogate area.
if value2&0xFC00 != 0xDC00 {
return yaml_parser_set_reader_error(parser,
"expected low surrogate area",
parser.offset+2, int(value2))
}
// Generate the value of the surrogate pair.
value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF)
} else {
width = 2
}
default:
panic("impossible")
}
// Check if the character is in the allowed range:
// #x9 | #xA | #xD | [#x20-#x7E] (8 bit)
// | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit)
// | [#x10000-#x10FFFF] (32 bit)
switch {
case value == 0x09:
case value == 0x0A:
case value == 0x0D:
case value >= 0x20 && value <= 0x7E:
case value == 0x85:
case value >= 0xA0 && value <= 0xD7FF:
case value >= 0xE000 && value <= 0xFFFD:
case value >= 0x10000 && value <= 0x10FFFF:
default:
return yaml_parser_set_reader_error(parser,
"control characters are not allowed",
parser.offset, int(value))
}
// Move the raw pointers.
parser.raw_buffer_pos += width
parser.offset += width
// Finally put the character into the buffer.
if value <= 0x7F {
// 0000 0000-0000 007F . 0xxxxxxx
parser.buffer[buffer_len+0] = byte(value)
} else if value <= 0x7FF {
// 0000 0080-0000 07FF . 110xxxxx 10xxxxxx
parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6))
parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F))
} else if value <= 0xFFFF {
// 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx
parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12))
parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F))
parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F))
} else {
// 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18))
parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F))
parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F))
parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F))
}
buffer_len += width
parser.unread++
}
// On EOF, put NUL into the buffer and return.
if parser.eof {
parser.buffer[buffer_len] = 0
buffer_len++
parser.unread++
break
}
}
parser.buffer = parser.buffer[:buffer_len]
return true
}

View File

@@ -0,0 +1,148 @@
package yaml
import (
"math"
"strconv"
"strings"
)
// TODO: merge, timestamps, base 60 floats, omap.
type resolveMapItem struct {
value interface{}
tag string
}
var resolveTable = make([]byte, 256)
var resolveMap = make(map[string]resolveMapItem)
func init() {
t := resolveTable
t[int('+')] = 'S' // Sign
t[int('-')] = 'S'
for _, c := range "0123456789" {
t[int(c)] = 'D' // Digit
}
for _, c := range "yYnNtTfFoO~" {
t[int(c)] = 'M' // In map
}
t[int('.')] = '.' // Float (potentially in map)
t[int('<')] = '<' // Merge
var resolveMapList = []struct {
v interface{}
tag string
l []string
}{
{true, "!!bool", []string{"y", "Y", "yes", "Yes", "YES"}},
{true, "!!bool", []string{"true", "True", "TRUE"}},
{true, "!!bool", []string{"on", "On", "ON"}},
{false, "!!bool", []string{"n", "N", "no", "No", "NO"}},
{false, "!!bool", []string{"false", "False", "FALSE"}},
{false, "!!bool", []string{"off", "Off", "OFF"}},
{nil, "!!null", []string{"~", "null", "Null", "NULL"}},
{math.NaN(), "!!float", []string{".nan", ".NaN", ".NAN"}},
{math.Inf(+1), "!!float", []string{".inf", ".Inf", ".INF"}},
{math.Inf(+1), "!!float", []string{"+.inf", "+.Inf", "+.INF"}},
{math.Inf(-1), "!!float", []string{"-.inf", "-.Inf", "-.INF"}},
{"<<", "!!merge", []string{"<<"}},
}
m := resolveMap
for _, item := range resolveMapList {
for _, s := range item.l {
m[s] = resolveMapItem{item.v, item.tag}
}
}
}
const longTagPrefix = "tag:yaml.org,2002:"
func shortTag(tag string) string {
if strings.HasPrefix(tag, longTagPrefix) {
return "!!" + tag[len(longTagPrefix):]
}
return tag
}
func resolvableTag(tag string) bool {
switch tag {
case "", "!!str", "!!bool", "!!int", "!!float", "!!null":
return true
}
return false
}
func resolve(tag string, in string) (rtag string, out interface{}) {
tag = shortTag(tag)
if !resolvableTag(tag) {
return tag, in
}
defer func() {
if tag != "" && tag != rtag {
panic("Can't decode " + rtag + " '" + in + "' as a " + tag)
}
}()
if in == "" {
return "!!null", nil
}
c := resolveTable[in[0]]
if c == 0 {
// It's a string for sure. Nothing to do.
return "!!str", in
}
// Handle things we can lookup in a map.
if item, ok := resolveMap[in]; ok {
return item.tag, item.value
}
switch c {
case 'M':
// We've already checked the map above.
case '.':
// Not in the map, so maybe a normal float.
floatv, err := strconv.ParseFloat(in, 64)
if err == nil {
return "!!float", floatv
}
// XXX Handle base 60 floats here (WTF!)
case 'D', 'S':
// Int, float, or timestamp.
plain := strings.Replace(in, "_", "", -1)
intv, err := strconv.ParseInt(plain, 0, 64)
if err == nil {
if intv == int64(int(intv)) {
return "!!int", int(intv)
} else {
return "!!int", intv
}
}
floatv, err := strconv.ParseFloat(plain, 64)
if err == nil {
return "!!float", floatv
}
if strings.HasPrefix(plain, "0b") {
intv, err := strconv.ParseInt(plain[2:], 2, 64)
if err == nil {
return "!!int", int(intv)
}
} else if strings.HasPrefix(plain, "-0b") {
intv, err := strconv.ParseInt(plain[3:], 2, 64)
if err == nil {
return "!!int", -int(intv)
}
}
// XXX Handle timestamps here.
default:
panic("resolveTable item not yet handled: " +
string([]byte{c}) + " (with " + in + ")")
}
return "!!str", in
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
package yaml
import (
"reflect"
"unicode"
)
type keyList []reflect.Value
func (l keyList) Len() int { return len(l) }
func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l keyList) Less(i, j int) bool {
a := l[i]
b := l[j]
ak := a.Kind()
bk := b.Kind()
for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() {
a = a.Elem()
ak = a.Kind()
}
for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() {
b = b.Elem()
bk = b.Kind()
}
af, aok := keyFloat(a)
bf, bok := keyFloat(b)
if aok && bok {
if af != bf {
return af < bf
}
if ak != bk {
return ak < bk
}
return numLess(a, b)
}
if ak != reflect.String || bk != reflect.String {
return ak < bk
}
ar, br := []rune(a.String()), []rune(b.String())
for i := 0; i < len(ar) && i < len(br); i++ {
if ar[i] == br[i] {
continue
}
al := unicode.IsLetter(ar[i])
bl := unicode.IsLetter(br[i])
if al && bl {
return ar[i] < br[i]
}
if al || bl {
return bl
}
var ai, bi int
var an, bn int64
for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ {
an = an*10 + int64(ar[ai]-'0')
}
for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ {
bn = bn*10 + int64(br[bi]-'0')
}
if an != bn {
return an < bn
}
if ai != bi {
return ai < bi
}
return ar[i] < br[i]
}
return len(ar) < len(br)
}
// keyFloat returns a float value for v if it is a number/bool
// and whether it is a number/bool or not.
func keyFloat(v reflect.Value) (f float64, ok bool) {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), true
case reflect.Float32, reflect.Float64:
return v.Float(), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return float64(v.Uint()), true
case reflect.Bool:
if v.Bool() {
return 1, true
}
return 0, true
}
return 0, false
}
// numLess returns whether a < b.
// a and b must necessarily have the same kind.
func numLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return a.Int() < b.Int()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Bool:
return !a.Bool() && b.Bool()
}
panic("not a number")
}

View File

@@ -0,0 +1,12 @@
package yaml_test
import (
. "gopkg.in/check.v1"
"testing"
)
func Test(t *testing.T) { TestingT(t) }
type S struct{}
var _ = Suite(&S{})

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