Merge remote-tracking branch 'dotcloud/master' into dhrp/docs

This commit is contained in:
Thatcher Peskens 2013-04-01 18:11:46 -07:00
commit f15889461d
25 changed files with 1285 additions and 317 deletions

View File

@ -7,6 +7,7 @@ Caleb Spare <cespare@gmail.com>
Charles Hooper <charles.hooper@dotcloud.com> Charles Hooper <charles.hooper@dotcloud.com>
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
Daniel Robinson <gottagetmac@gmail.com> Daniel Robinson <gottagetmac@gmail.com>
Dominik Honnef <dominik@honnef.co>
Don Spaulding <donspauldingii@gmail.com> Don Spaulding <donspauldingii@gmail.com>
ezbercih <cem.ezberci@gmail.com> ezbercih <cem.ezberci@gmail.com>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu> Frederick F. Kautz IV <fkautz@alumni.cmu.edu>

69
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,69 @@
# Contributing to Docker
Want to hack on Docker? Awesome! There are instructions to get you
started on the website: http://docker.io/gettingstarted.html
They are probably not perfect, please let us know if anything feels
wrong or incomplete.
## Contribution guidelines
### Pull requests are always welcome
We are always thrilled to receive pull requests, and do our best to
process them as fast as possible. Not sure if that typo is worth a pull
request? Do it! We will appreciate it.
If your pull request is not accepted on the first try, don't be
discouraged! If there's a problem with the implementation, hopefully you
received feedback on what to improve.
We're trying very hard to keep Docker lean and focused. We don't want it
to do everything for everybody. This means that we might decide against
incorporating a new feature. However, there might be a way to implement
that feature *on top of* docker.
### Discuss your design on the mailing list
We recommend discussing your plans [on the mailing
list](https://groups.google.com/forum/?fromgroups#!forum/docker-club)
before starting to code - especially for more ambitious contributions.
This gives other contributors a chance to point you in the right
direction, give feedback on your design, and maybe point out if someone
else is working on the same thing.
### Create issues...
Any significant improvement should be documented as [a github
issue](https://github.com/dotcloud/docker/issues) before anybody
starts working on it.
### ...but check for existing issues first!
Please take a moment to check that an issue doesn't already exist
documenting your bug report or improvement proposal. If it does, it
never hurts to add a quick "+1" or "I have this problem too". This will
help prioritize the most common problems and requests.
### Conventions
Fork the repo and make changes on your fork in a feature branch:
- If it's a bugfix branch, name it XXX-something where XXX is the number of the issue
- If it's a feature branch, create an enhancement issue to announce your intentions, and name it XXX-something where XXX is the number of the issue.
Submit unit tests for your changes. Golang has a great testing suite built
in: use it! Take a look at existing tests for inspiration. Run the full test
suite against your change and the master.
Submit any relevant updates or additions to documentation.
Add clean code:
- Universally formatted code promotes ease of writing, reading, and maintenance. We suggest using gofmt before committing your changes. There's a git pre-commit hook made for doing so.
- curl -o .git/hooks/pre-commit https://raw.github.com/edsrzf/gofmt-git-hook/master/fmt-check && chmod +x .git/hooks/pre-commit
Pull requests descriptions should be as clear as possible and include a
referenced to all the issues that they address.
Add your name to the AUTHORS file.

View File

@ -41,3 +41,6 @@ endif
test: all test: all
@(cd $(DOCKER_DIR); sudo -E go test $(GO_OPTIONS)) @(cd $(DOCKER_DIR); sudo -E go test $(GO_OPTIONS))
fmt:
@gofmt -s -l -w .

View File

@ -192,11 +192,10 @@ echo "Daemon received: $(docker logs $JOB)"
Contributing to Docker Contributing to Docker
====================== ======================
Want to hack on Docker? Awesome! There are instructions to get you started on the website: http://docker.io/documentation/contributing/contributing.html Want to hack on Docker? Awesome! There are instructions to get you started on the website: http://docs.docker.io/en/latest/contributing/contributing/
They are probably not perfect, please let us know if anything feels wrong or incomplete. They are probably not perfect, please let us know if anything feels wrong or incomplete.
### Pull requests are always welcome
Note Note
---- ----
@ -206,26 +205,6 @@ Please find it under docs/sources/ and read more about it https://github.com/dot
Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome. Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome.
### Discuss your design on the mailing list
We recommend discussing your plans [on the mailing list](https://groups.google.com/forum/?fromgroups#!forum/docker-club) before starting to code - especially for more ambitious contributions. This gives other contributors a chance to point
you in the right direction, give feedback on your design, and maybe point out if someone else is working on the same thing.
### Create issues...
Any significant improvement should be documented as [a github issue](https://github.com/dotcloud/docker/issues) before anybody starts working on it.
### ...but check for existing issues first!
Please take a moment to check that an issue doesn't already exist documenting your bug report or improvement proposal.
If it does, it never hurts to add a quick "+1" or "I have this problem too". This will help prioritize the most common problems and requests.
### Write tests
Golang has a great testing suite built in: use it! Take a look at existing tests for inspiration.
Setting up a dev environment Setting up a dev environment
---------------------------- ----------------------------

View File

@ -6,20 +6,28 @@ import (
"os" "os"
"os/exec" "os/exec"
"testing" "testing"
"time"
) )
func TestCmdStreamLargeStderr(t *testing.T) { func TestCmdStreamLargeStderr(t *testing.T) {
// This test checks for deadlock; thus, the main failure mode of this test is deadlocking.
// FIXME implement a timeout to avoid blocking the whole test suite when this test fails
cmd := exec.Command("/bin/sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello") cmd := exec.Command("/bin/sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello")
out, err := CmdStream(cmd) out, err := CmdStream(cmd)
if err != nil { if err != nil {
t.Fatalf("Failed to start command: " + err.Error()) t.Fatalf("Failed to start command: " + err.Error())
} }
_, err = io.Copy(ioutil.Discard, out) errCh := make(chan error)
go func() {
_, err := io.Copy(ioutil.Discard, out)
errCh <- err
}()
select {
case err := <-errCh:
if err != nil { if err != nil {
t.Fatalf("Command should not have failed (err=%s...)", err.Error()[:100]) t.Fatalf("Command should not have failed (err=%s...)", err.Error()[:100])
} }
case <-time.After(5 * time.Second):
t.Fatalf("Command did not complete in 5 seconds; probable deadlock")
}
} }
func TestCmdStreamBad(t *testing.T) { func TestCmdStreamBad(t *testing.T) {

View File

@ -1,6 +1,7 @@
package auth package auth
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@ -111,7 +112,7 @@ func Login(authConfig *AuthConfig) (string, error) {
return "", errors.New(errMsg) return "", errors.New(errMsg)
} }
b := strings.NewReader(string(jsonBody)) b := bytes.NewReader(jsonBody)
req1, err := http.Post(REGISTRY_SERVER+"/v1/users", "application/json; charset=utf-8", b) req1, err := http.Post(REGISTRY_SERVER+"/v1/users", "application/json; charset=utf-8", b)
if err != nil { if err != nil {
errMsg = fmt.Sprintf("Server Error: %s", err) errMsg = fmt.Sprintf("Server Error: %s", err)
@ -130,6 +131,7 @@ func Login(authConfig *AuthConfig) (string, error) {
status = "Account Created\n" status = "Account Created\n"
storeConfig = true storeConfig = true
} else if reqStatusCode == 400 { } else if reqStatusCode == 400 {
// FIXME: This should be 'exists', not 'exist'. Need to change on the server first.
if string(reqBody) == "Username or email already exist" { if string(reqBody) == "Username or email already exist" {
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("GET", REGISTRY_SERVER+"/v1/users", nil) req, err := http.NewRequest("GET", REGISTRY_SERVER+"/v1/users", nil)
@ -151,11 +153,11 @@ func Login(authConfig *AuthConfig) (string, error) {
return "", errors.New(status) return "", errors.New(status)
} }
} else { } else {
status = fmt.Sprintf("Registration: %s", string(reqBody)) status = fmt.Sprintf("Registration: %s", reqBody)
return "", errors.New(status) return "", errors.New(status)
} }
} else { } else {
status = fmt.Sprintf("[%s] : %s", reqStatusCode, string(reqBody)) status = fmt.Sprintf("[%s] : %s", reqStatusCode, reqBody)
return "", errors.New(status) return "", errors.New(status)
} }
if storeConfig { if storeConfig {

View File

@ -3,7 +3,6 @@ package docker
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/dotcloud/docker/auth" "github.com/dotcloud/docker/auth"
"github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/rcli"
@ -17,9 +16,10 @@ import (
"sync" "sync"
"text/tabwriter" "text/tabwriter"
"time" "time"
"unicode"
) )
const VERSION = "0.1.0" const VERSION = "0.1.1"
func (srv *Server) Name() string { func (srv *Server) Name() string {
return "docker" return "docker"
@ -28,7 +28,7 @@ func (srv *Server) Name() string {
// FIXME: Stop violating DRY by repeating usage here and in Subcmd declarations // FIXME: Stop violating DRY by repeating usage here and in Subcmd declarations
func (srv *Server) Help() string { func (srv *Server) Help() string {
help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n" help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n"
for _, cmd := range [][]interface{}{ for _, cmd := range [][]string{
{"attach", "Attach to a running container"}, {"attach", "Attach to a running container"},
{"commit", "Create a new image from a container's changes"}, {"commit", "Create a new image from a container's changes"},
{"diff", "Inspect changes on a container's filesystem"}, {"diff", "Inspect changes on a container's filesystem"},
@ -62,29 +62,80 @@ func (srv *Server) Help() string {
// 'docker login': login / register a user to registry service. // 'docker login': login / register a user to registry service.
func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error { func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
// Read a line on raw terminal with support for simple backspace
// sequences and echo.
//
// This function is necessary because the login command must be done in a
// raw terminal for two reasons:
// - we have to read a password (without echoing it);
// - the rcli "protocol" only supports cannonical and raw modes and you
// can't tune it once the command as been started.
var readStringOnRawTerminal = func(stdin io.Reader, stdout io.Writer, echo bool) string {
char := make([]byte, 1)
buffer := make([]byte, 64)
var i = 0
for i < len(buffer) {
n, err := stdin.Read(char)
if n > 0 {
if char[0] == '\r' || char[0] == '\n' {
stdout.Write([]byte{'\n'})
break
} else if char[0] == 127 || char[0] == '\b' {
if i > 0 {
if echo {
stdout.Write([]byte{'\b', ' ', '\b'})
}
i--
}
} else if !unicode.IsSpace(rune(char[0])) &&
!unicode.IsControl(rune(char[0])) {
if echo {
stdout.Write(char)
}
buffer[i] = char[0]
i++
}
}
if err != nil {
if err != io.EOF {
fmt.Fprint(stdout, "Read error: %v\n", err)
}
break
}
}
return string(buffer[:i])
}
var readAndEchoString = func(stdin io.Reader, stdout io.Writer) string {
return readStringOnRawTerminal(stdin, stdout, true)
}
var readString = func(stdin io.Reader, stdout io.Writer) string {
return readStringOnRawTerminal(stdin, stdout, false)
}
cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server") cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server")
if err := cmd.Parse(args); err != nil { if err := cmd.Parse(args); err != nil {
return nil return nil
} }
var username string var username string
var password string var password string
var email string var email string
fmt.Fprint(stdout, "Username (", srv.runtime.authConfig.Username, "): ") fmt.Fprint(stdout, "Username (", srv.runtime.authConfig.Username, "): ")
fmt.Fscanf(stdin, "%s", &username) username = readAndEchoString(stdin, stdout)
if username == "" { if username == "" {
username = srv.runtime.authConfig.Username username = srv.runtime.authConfig.Username
} }
if username != srv.runtime.authConfig.Username { if username != srv.runtime.authConfig.Username {
fmt.Fprint(stdout, "Password: ") fmt.Fprint(stdout, "Password: ")
fmt.Fscanf(stdin, "%s", &password) password = readString(stdin, stdout)
if password == "" { if password == "" {
return errors.New("Error : Password Required\n") return fmt.Errorf("Error : Password Required")
} }
fmt.Fprint(stdout, "Email (", srv.runtime.authConfig.Email, "): ") fmt.Fprint(stdout, "Email (", srv.runtime.authConfig.Email, "): ")
fmt.Fscanf(stdin, "%s", &email) email = readAndEchoString(stdin, stdout)
if email == "" { if email == "" {
email = srv.runtime.authConfig.Email email = srv.runtime.authConfig.Email
} }
@ -95,12 +146,12 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin
newAuthConfig := auth.NewAuthConfig(username, password, email, srv.runtime.root) newAuthConfig := auth.NewAuthConfig(username, password, email, srv.runtime.root)
status, err := auth.Login(newAuthConfig) status, err := auth.Login(newAuthConfig)
if err != nil { if err != nil {
fmt.Fprintf(stdout, "Error : %s\n", err) fmt.Fprintln(stdout, "Error:", err)
} else { } else {
srv.runtime.authConfig = newAuthConfig srv.runtime.authConfig = newAuthConfig
} }
if status != "" { if status != "" {
fmt.Fprintf(stdout, status) fmt.Fprint(stdout, status)
} }
return nil return nil
} }
@ -119,7 +170,7 @@ func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string
if container := srv.runtime.Get(name); container != nil { if container := srv.runtime.Get(name); container != nil {
fmt.Fprintln(stdout, container.Wait()) fmt.Fprintln(stdout, container.Wait())
} else { } else {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
} }
return nil return nil
@ -152,6 +203,12 @@ func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string
len(srv.runtime.List()), len(srv.runtime.List()),
VERSION, VERSION,
imgcount) imgcount)
if !rcli.DEBUG_FLAG {
return nil
}
fmt.Fprintln(stdout, "debug mode enabled")
fmt.Fprintf(stdout, "fds: %d\ngoroutines: %d\n", getTotalUsedFds(), runtime.NumGoroutine())
return nil return nil
} }
@ -169,9 +226,9 @@ func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string
if err := container.Stop(); err != nil { if err := container.Stop(); err != nil {
return err return err
} }
fmt.Fprintln(stdout, container.Id) fmt.Fprintln(stdout, container.ShortId())
} else { } else {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
} }
return nil return nil
@ -191,9 +248,9 @@ func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...str
if err := container.Restart(); err != nil { if err := container.Restart(); err != nil {
return err return err
} }
fmt.Fprintln(stdout, container.Id) fmt.Fprintln(stdout, container.ShortId())
} else { } else {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
} }
return nil return nil
@ -213,9 +270,9 @@ func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...strin
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
return err return err
} }
fmt.Fprintln(stdout, container.Id) fmt.Fprintln(stdout, container.ShortId())
} else { } else {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
} }
return nil return nil
@ -268,7 +325,7 @@ func (srv *Server) CmdPort(stdin io.ReadCloser, stdout io.Writer, args ...string
name := cmd.Arg(0) name := cmd.Arg(0)
privatePort := cmd.Arg(1) privatePort := cmd.Arg(1)
if container := srv.runtime.Get(name); container == nil { if container := srv.runtime.Get(name); container == nil {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} else { } else {
if frontend, exists := container.NetworkSettings.PortMapping[privatePort]; !exists { if frontend, exists := container.NetworkSettings.PortMapping[privatePort]; !exists {
return fmt.Errorf("No private port '%s' allocated on %s", privatePort, name) return fmt.Errorf("No private port '%s' allocated on %s", privatePort, name)
@ -312,10 +369,10 @@ func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout io.Writer, args ...str
} }
w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
defer w.Flush() defer w.Flush()
fmt.Fprintf(w, "ID\tCREATED\tCREATED BY\n") fmt.Fprintln(w, "ID\tCREATED\tCREATED BY")
return image.WalkHistory(func(img *Image) error { return image.WalkHistory(func(img *Image) error {
fmt.Fprintf(w, "%s\t%s\t%s\n", fmt.Fprintf(w, "%s\t%s\t%s\n",
srv.runtime.repositories.ImageName(img.Id), srv.runtime.repositories.ImageName(img.ShortId()),
HumanDuration(time.Now().Sub(img.Created))+" ago", HumanDuration(time.Now().Sub(img.Created))+" ago",
strings.Join(img.ContainerConfig.Cmd, " "), strings.Join(img.ContainerConfig.Cmd, " "),
) )
@ -331,7 +388,7 @@ func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string)
for _, name := range cmd.Args() { for _, name := range cmd.Args() {
container := srv.runtime.Get(name) container := srv.runtime.Get(name)
if container == nil { if container == nil {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
if err := srv.runtime.Destroy(container); err != nil { if err := srv.runtime.Destroy(container); err != nil {
fmt.Fprintln(stdout, "Error destroying container "+name+": "+err.Error()) fmt.Fprintln(stdout, "Error destroying container "+name+": "+err.Error())
@ -349,7 +406,7 @@ func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string
for _, name := range cmd.Args() { for _, name := range cmd.Args() {
container := srv.runtime.Get(name) container := srv.runtime.Get(name)
if container == nil { if container == nil {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
if err := container.Kill(); err != nil { if err := container.Kill(); err != nil {
fmt.Fprintln(stdout, "Error killing container "+name+": "+err.Error()) fmt.Fprintln(stdout, "Error killing container "+name+": "+err.Error())
@ -368,7 +425,7 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...stri
} }
src := cmd.Arg(0) src := cmd.Arg(0)
if src == "" { if src == "" {
return errors.New("Not enough arguments") return fmt.Errorf("Not enough arguments")
} else if src == "-" { } else if src == "-" {
archive = stdin archive = stdin
} else { } else {
@ -381,7 +438,7 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...stri
u.Host = src u.Host = src
u.Path = "" u.Path = ""
} }
fmt.Fprintf(stdout, "Downloading from %s\n", u.String()) fmt.Fprintln(stdout, "Downloading from", u)
// Download with curl (pretty progress bar) // Download with curl (pretty progress bar)
// If curl is not available, fallback to http.Get() // If curl is not available, fallback to http.Get()
resp, err = Download(u.String(), stdout) resp, err = Download(u.String(), stdout)
@ -401,7 +458,7 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...stri
return err return err
} }
} }
fmt.Fprintln(stdout, img.Id) fmt.Fprintln(stdout, img.ShortId())
return nil return nil
} }
@ -507,7 +564,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
} }
w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
if !*quiet { if !*quiet {
fmt.Fprintf(w, "REPOSITORY\tTAG\tID\tCREATED\tPARENT\n") fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED\tPARENT")
} }
var allImages map[string]*Image var allImages map[string]*Image
var err error var err error
@ -534,7 +591,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
for idx, field := range []string{ for idx, field := range []string{
/* REPOSITORY */ name, /* REPOSITORY */ name,
/* TAG */ tag, /* TAG */ tag,
/* ID */ id, /* ID */ TruncateId(id),
/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago", /* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
/* PARENT */ srv.runtime.repositories.ImageName(image.Parent), /* PARENT */ srv.runtime.repositories.ImageName(image.Parent),
} { } {
@ -546,7 +603,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
} }
w.Write([]byte{'\n'}) w.Write([]byte{'\n'})
} else { } else {
stdout.Write([]byte(image.Id + "\n")) stdout.Write([]byte(image.ShortId() + "\n"))
} }
} }
} }
@ -557,7 +614,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
for idx, field := range []string{ for idx, field := range []string{
/* REPOSITORY */ "<none>", /* REPOSITORY */ "<none>",
/* TAG */ "<none>", /* TAG */ "<none>",
/* ID */ id, /* ID */ TruncateId(id),
/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago", /* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
/* PARENT */ srv.runtime.repositories.ImageName(image.Parent), /* PARENT */ srv.runtime.repositories.ImageName(image.Parent),
} { } {
@ -569,7 +626,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
} }
w.Write([]byte{'\n'}) w.Write([]byte{'\n'})
} else { } else {
stdout.Write([]byte(image.Id + "\n")) stdout.Write([]byte(image.ShortId() + "\n"))
} }
} }
} }
@ -590,7 +647,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string)
} }
w := tabwriter.NewWriter(stdout, 12, 1, 3, ' ', 0) w := tabwriter.NewWriter(stdout, 12, 1, 3, ' ', 0)
if !*quiet { if !*quiet {
fmt.Fprintf(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tCOMMENT\n") fmt.Fprintln(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tCOMMENT")
} }
for _, container := range srv.runtime.List() { for _, container := range srv.runtime.List() {
if !container.State.Running && !*flAll { if !container.State.Running && !*flAll {
@ -602,7 +659,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string)
command = Trunc(command, 20) command = Trunc(command, 20)
} }
for idx, field := range []string{ for idx, field := range []string{
/* ID */ container.Id, /* ID */ container.ShortId(),
/* IMAGE */ srv.runtime.repositories.ImageName(container.Image), /* IMAGE */ srv.runtime.repositories.ImageName(container.Image),
/* COMMAND */ command, /* COMMAND */ command,
/* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago", /* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago",
@ -617,7 +674,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string)
} }
w.Write([]byte{'\n'}) w.Write([]byte{'\n'})
} else { } else {
stdout.Write([]byte(container.Id + "\n")) stdout.Write([]byte(container.ShortId() + "\n"))
} }
} }
if !*quiet { if !*quiet {
@ -643,7 +700,7 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(stdout, img.Id) fmt.Fprintln(stdout, img.ShortId())
return nil return nil
} }
@ -666,7 +723,7 @@ func (srv *Server) CmdExport(stdin io.ReadCloser, stdout io.Writer, args ...stri
} }
return nil return nil
} }
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
@ -677,10 +734,10 @@ func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string
return nil return nil
} }
if cmd.NArg() < 1 { if cmd.NArg() < 1 {
return errors.New("Not enough arguments") return fmt.Errorf("Not enough arguments")
} }
if container := srv.runtime.Get(cmd.Arg(0)); container == nil { if container := srv.runtime.Get(cmd.Arg(0)); container == nil {
return errors.New("No such container") return fmt.Errorf("No such container")
} else { } else {
changes, err := container.Changes() changes, err := container.Changes()
if err != nil { if err != nil {
@ -722,14 +779,11 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string
} }
return nil return nil
} }
return errors.New("No such container: " + cmd.Arg(0)) return fmt.Errorf("No such container: %s", cmd.Arg(0))
} }
func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...string) error { func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
cmd := rcli.Subcmd(stdout, "attach", "[OPTIONS]", "Attach to a running container") cmd := rcli.Subcmd(stdout, "attach", "CONTAINER", "Attach to a running container")
flStdin := cmd.Bool("i", false, "Attach to stdin")
flStdout := cmd.Bool("o", true, "Attach to stdout")
flStderr := cmd.Bool("e", true, "Attach to stderr")
if err := cmd.Parse(args); err != nil { if err := cmd.Parse(args); err != nil {
return nil return nil
} }
@ -740,33 +794,56 @@ func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...stri
name := cmd.Arg(0) name := cmd.Arg(0)
container := srv.runtime.Get(name) container := srv.runtime.Get(name)
if container == nil { if container == nil {
return errors.New("No such container: " + name) return fmt.Errorf("No such container: %s", name)
} }
cStdout, err := container.StdoutPipe()
if err != nil {
return err
}
cStderr, err := container.StderrPipe()
if err != nil {
return err
}
var wg sync.WaitGroup var wg sync.WaitGroup
if *flStdin { if container.Config.OpenStdin {
cStdin, err := container.StdinPipe() cStdin, err := container.StdinPipe()
if err != nil { if err != nil {
return err return err
} }
wg.Add(1) wg.Add(1)
go func() { io.Copy(cStdin, stdin); wg.Add(-1) }() go func() {
Debugf("Begin stdin pipe [attach]")
io.Copy(cStdin, stdin)
// When stdin get closed, it means the client has been detached
// Make sure all pipes are closed.
if err := cStdout.Close(); err != nil {
Debugf("Error closing stdin pipe: %s", err)
} }
if *flStdout { if err := cStderr.Close(); err != nil {
cStdout, err := container.StdoutPipe() Debugf("Error closing stderr pipe: %s", err)
if err != nil { }
return err
wg.Add(-1)
Debugf("End of stdin pipe [attach]")
}()
} }
wg.Add(1) wg.Add(1)
go func() { io.Copy(stdout, cStdout); wg.Add(-1) }() go func() {
} Debugf("Begin stdout pipe [attach]")
if *flStderr { io.Copy(stdout, cStdout)
cStderr, err := container.StderrPipe() wg.Add(-1)
if err != nil { Debugf("End of stdout pipe [attach]")
return err }()
}
wg.Add(1) wg.Add(1)
go func() { io.Copy(stdout, cStderr); wg.Add(-1) }() go func() {
} Debugf("Begin stderr pipe [attach]")
io.Copy(stdout, cStderr)
wg.Add(-1)
Debugf("End of stderr pipe [attach]")
}()
wg.Wait() wg.Wait()
return nil return nil
} }
@ -889,7 +966,7 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string)
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
return err return err
} }
fmt.Fprintln(stdout, container.Id) fmt.Fprintln(stdout, container.ShortId())
} }
return nil return nil
} }

168
commands_test.go Normal file
View File

@ -0,0 +1,168 @@
package docker
import (
"bufio"
"fmt"
"io"
"strings"
"testing"
"time"
)
func closeWrap(args ...io.Closer) error {
e := false
ret := fmt.Errorf("Error closing elements")
for _, c := range args {
if err := c.Close(); err != nil {
e = true
ret = fmt.Errorf("%s\n%s", ret, err)
}
}
if e {
return ret
}
return nil
}
func setTimeout(t *testing.T, msg string, d time.Duration, f func()) {
c := make(chan bool)
// Make sure we are not too long
go func() {
time.Sleep(d)
c <- true
}()
go func() {
f()
c <- false
}()
if <-c {
t.Fatal(msg)
}
}
func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error {
for i := 0; i < count; i++ {
if _, err := w.Write([]byte(input)); err != nil {
return err
}
o, err := bufio.NewReader(r).ReadString('\n')
if err != nil {
return err
}
if strings.Trim(o, " \r\n") != output {
return fmt.Errorf("Unexpected output. Expected [%s], received [%s]", output, o)
}
}
return nil
}
// Expected behaviour: the process dies when the client disconnects
func TestRunDisconnect(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
srv := &Server{runtime: runtime}
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
c1 := make(chan struct{})
go func() {
if err := srv.CmdRun(stdin, stdoutPipe, "-i", GetTestImage(runtime).Id, "/bin/cat"); err != nil {
t.Fatal(err)
}
close(c1)
}()
setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
t.Fatal(err)
}
})
// Close pipes (simulate disconnect)
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
t.Fatal(err)
}
// as the pipes are close, we expect the process to die,
// therefore CmdRun to unblock. Wait for CmdRun
setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() {
<-c1
})
// Check the status of the container
container := runtime.containers.Back().Value.(*Container)
if container.State.Running {
t.Fatalf("/bin/cat is still running after closing stdin")
}
}
// Expected behaviour, the process stays alive when the client disconnects
func TestAttachDisconnect(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
srv := &Server{runtime: runtime}
container, err := runtime.Create(
&Config{
Image: GetTestImage(runtime).Id,
Memory: 33554432,
Cmd: []string{"/bin/cat"},
OpenStdin: true,
},
)
if err != nil {
t.Fatal(err)
}
defer runtime.Destroy(container)
// Start the process
if err := container.Start(); err != nil {
t.Fatal(err)
}
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
// Attach to it
c1 := make(chan struct{})
go func() {
if err := srv.CmdAttach(stdin, stdoutPipe, container.Id); err != nil {
t.Fatal(err)
}
close(c1)
}()
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
t.Fatal(err)
}
})
// Close pipes (client disconnects)
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
t.Fatal(err)
}
// Wait for attach to finish, the client disconnected, therefore, Attach finished his job
setTimeout(t, "Waiting for CmdAttach timed out", 2*time.Second, func() {
<-c1
})
// We closed stdin, expect /bin/cat to still be running
// Wait a little bit to make sure container.monitor() did his thing
err = container.WaitTimeout(500 * time.Millisecond)
if err == nil || !container.State.Running {
t.Fatalf("/bin/cat is not running after closing stdin")
}
// Try to avoid the timeoout in destroy. Best effort, don't check error
cStdin, _ := container.StdinPipe()
cStdin.Close()
}

View File

@ -2,7 +2,6 @@ package docker
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/rcli"
"github.com/kr/pty" "github.com/kr/pty"
@ -41,8 +40,10 @@ type Container struct {
stdin io.ReadCloser stdin io.ReadCloser
stdinPipe io.WriteCloser stdinPipe io.WriteCloser
stdoutLog *os.File ptyStdinMaster io.Closer
stderrLog *os.File ptyStdoutMaster io.Closer
ptyStderrMaster io.Closer
runtime *Runtime runtime *Runtime
} }
@ -154,39 +155,49 @@ func (container *Container) startPty() error {
if err != nil { if err != nil {
return err return err
} }
container.ptyStdoutMaster = stdoutMaster
container.cmd.Stdout = stdoutSlave container.cmd.Stdout = stdoutSlave
stderrMaster, stderrSlave, err := pty.Open() stderrMaster, stderrSlave, err := pty.Open()
if err != nil { if err != nil {
return err return err
} }
container.ptyStderrMaster = stderrMaster
container.cmd.Stderr = stderrSlave container.cmd.Stderr = stderrSlave
// Copy the PTYs to our broadcasters // Copy the PTYs to our broadcasters
go func() { go func() {
defer container.stdout.Close() defer container.stdout.Close()
Debugf("[startPty] Begin of stdout pipe")
io.Copy(container.stdout, stdoutMaster) io.Copy(container.stdout, stdoutMaster)
Debugf("[startPty] End of stdout pipe")
}() }()
go func() { go func() {
defer container.stderr.Close() defer container.stderr.Close()
Debugf("[startPty] Begin of stderr pipe")
io.Copy(container.stderr, stderrMaster) io.Copy(container.stderr, stderrMaster)
Debugf("[startPty] End of stderr pipe")
}() }()
// stdin // stdin
var stdinSlave io.ReadCloser var stdinSlave io.ReadCloser
if container.Config.OpenStdin { if container.Config.OpenStdin {
stdinMaster, stdinSlave, err := pty.Open() var stdinMaster io.WriteCloser
stdinMaster, stdinSlave, err = pty.Open()
if err != nil { if err != nil {
return err return err
} }
container.ptyStdinMaster = stdinMaster
container.cmd.Stdin = stdinSlave container.cmd.Stdin = stdinSlave
// FIXME: The following appears to be broken. // FIXME: The following appears to be broken.
// "cannot set terminal process group (-1): Inappropriate ioctl for device" // "cannot set terminal process group (-1): Inappropriate ioctl for device"
// container.cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true} // container.cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
go func() { go func() {
defer container.stdin.Close() defer container.stdin.Close()
Debugf("[startPty] Begin of stdin pipe")
io.Copy(stdinMaster, container.stdin) io.Copy(stdinMaster, container.stdin)
Debugf("[startPty] End of stdin pipe")
}() }()
} }
if err := container.cmd.Start(); err != nil { if err := container.cmd.Start(); err != nil {
@ -210,13 +221,18 @@ func (container *Container) start() error {
} }
go func() { go func() {
defer stdin.Close() defer stdin.Close()
Debugf("Begin of stdin pipe [start]")
io.Copy(stdin, container.stdin) io.Copy(stdin, container.stdin)
Debugf("End of stdin pipe [start]")
}() }()
} }
return container.cmd.Start() return container.cmd.Start()
} }
func (container *Container) Start() error { func (container *Container) Start() error {
if container.State.Running {
return fmt.Errorf("The container %s is already running.", container.Id)
}
if err := container.EnsureMounted(); err != nil { if err := container.EnsureMounted(); err != nil {
return err return err
} }
@ -256,6 +272,14 @@ func (container *Container) Start() error {
container.Config.Env..., container.Config.Env...,
) )
// Setup logging of stdout and stderr to disk
if err := container.runtime.LogToDisk(container.stdout, container.logPath("stdout")); err != nil {
return err
}
if err := container.runtime.LogToDisk(container.stderr, container.logPath("stderr")); err != nil {
return err
}
var err error var err error
if container.Config.Tty { if container.Config.Tty {
container.cmd.Env = append( container.cmd.Env = append(
@ -339,24 +363,53 @@ func (container *Container) allocateNetwork() error {
return nil return nil
} }
func (container *Container) releaseNetwork() error { func (container *Container) releaseNetwork() {
err := container.network.Release() container.network.Release()
container.network = nil container.network = nil
container.NetworkSettings = &NetworkSettings{} container.NetworkSettings = &NetworkSettings{}
return err
} }
func (container *Container) monitor() { func (container *Container) monitor() {
// Wait for the program to exit // Wait for the program to exit
container.cmd.Wait() Debugf("Waiting for process")
if err := container.cmd.Wait(); err != nil {
// Discard the error as any signals or non 0 returns will generate an error
Debugf("%s: Process: %s", container.Id, err)
}
Debugf("Process finished")
exitCode := container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() exitCode := container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
// Cleanup // Cleanup
if err := container.releaseNetwork(); err != nil { container.releaseNetwork()
log.Printf("%v: Failed to release network: %v", container.Id, err) if container.Config.OpenStdin {
if err := container.stdin.Close(); err != nil {
Debugf("%s: Error close stdin: %s", container.Id, err)
} }
container.stdout.Close() }
container.stderr.Close() if err := container.stdout.Close(); err != nil {
Debugf("%s: Error close stdout: %s", container.Id, err)
}
if err := container.stderr.Close(); err != nil {
Debugf("%s: Error close stderr: %s", container.Id, err)
}
if container.ptyStdinMaster != nil {
if err := container.ptyStdinMaster.Close(); err != nil {
Debugf("%s: Error close pty stdin master: %s", container.Id, err)
}
}
if container.ptyStdoutMaster != nil {
if err := container.ptyStdoutMaster.Close(); err != nil {
Debugf("%s: Error close pty stdout master: %s", container.Id, err)
}
}
if container.ptyStderrMaster != nil {
if err := container.ptyStderrMaster.Close(); err != nil {
Debugf("%s: Error close pty stderr master: %s", container.Id, err)
}
}
if err := container.Unmount(); err != nil { if err := container.Unmount(); err != nil {
log.Printf("%v: Failed to umount filesystem: %v", container.Id, err) log.Printf("%v: Failed to umount filesystem: %v", container.Id, err)
} }
@ -368,7 +421,15 @@ func (container *Container) monitor() {
// Report status back // Report status back
container.State.setStopped(exitCode) container.State.setStopped(exitCode)
container.ToDisk() if err := container.ToDisk(); err != nil {
// FIXME: there is a race condition here which causes this to fail during the unit tests.
// If another goroutine was waiting for Wait() to return before removing the container's root
// from the filesystem... At this point it may already have done so.
// This is because State.setStopped() has already been called, and has caused Wait()
// to return.
// FIXME: why are we serializing running state to disk in the first place?
//log.Printf("%s: Failed to dump configuration to the disk: %s", container.Id, err)
}
} }
func (container *Container) kill() error { func (container *Container) kill() error {
@ -397,8 +458,8 @@ func (container *Container) Stop() error {
// 1. Send a SIGTERM // 1. Send a SIGTERM
if output, err := exec.Command("lxc-kill", "-n", container.Id, "15").CombinedOutput(); err != nil { if output, err := exec.Command("lxc-kill", "-n", container.Id, "15").CombinedOutput(); err != nil {
log.Printf(string(output)) log.Print(string(output))
log.Printf("Failed to send SIGTERM to the process, force killing") log.Print("Failed to send SIGTERM to the process, force killing")
if err := container.Kill(); err != nil { if err := container.Kill(); err != nil {
return err return err
} }
@ -453,7 +514,7 @@ func (container *Container) WaitTimeout(timeout time.Duration) error {
select { select {
case <-time.After(timeout): case <-time.After(timeout):
return errors.New("Timed Out") return fmt.Errorf("Timed Out")
case <-done: case <-done:
return nil return nil
} }
@ -500,6 +561,14 @@ func (container *Container) Unmount() error {
return Unmount(container.RootfsPath()) return Unmount(container.RootfsPath())
} }
// ShortId returns a shorthand version of the container's id for convenience.
// A collision with other container shorthands is very unlikely, but possible.
// In case of a collision a lookup with Runtime.Get() will fail, and the caller
// will need to use a langer prefix, or the full-length container Id.
func (container *Container) ShortId() string {
return TruncateId(container.Id)
}
func (container *Container) logPath(name string) string { func (container *Container) logPath(name string) string {
return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name)) return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name))
} }

View File

@ -39,6 +39,117 @@ func TestIdFormat(t *testing.T) {
} }
} }
func TestMultipleAttachRestart(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
container, err := runtime.Create(
&Config{
Image: GetTestImage(runtime).Id,
Cmd: []string{"/bin/sh", "-c",
"i=1; while [ $i -le 5 ]; do i=`expr $i + 1`; echo hello; done"},
Memory: 33554432,
},
)
if err != nil {
t.Fatal(err)
}
defer runtime.Destroy(container)
// Simulate 3 client attaching to the container and stop/restart
stdout1, err := container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stdout2, err := container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stdout3, err := container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := container.Start(); err != nil {
t.Fatal(err)
}
l1, err := bufio.NewReader(stdout1).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l1, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l1)
}
l2, err := bufio.NewReader(stdout2).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l2, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l2)
}
l3, err := bufio.NewReader(stdout3).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l3, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l3)
}
if err := container.Stop(); err != nil {
t.Fatal(err)
}
stdout1, err = container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stdout2, err = container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stdout3, err = container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := container.Start(); err != nil {
t.Fatal(err)
}
timeout := make(chan bool)
go func() {
l1, err = bufio.NewReader(stdout1).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l1, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l1)
}
l2, err = bufio.NewReader(stdout2).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l2, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l2)
}
l3, err = bufio.NewReader(stdout3).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.Trim(l3, " \r\n") != "hello" {
t.Fatalf("Unexpected output. Expected [%s], received [%s]", "hello", l3)
}
timeout <- false
}()
go func() {
time.Sleep(3 * time.Second)
timeout <- true
}()
if <-timeout {
t.Fatalf("Timeout reading from the process")
}
}
func TestCommitRun(t *testing.T) { func TestCommitRun(t *testing.T) {
runtime, err := newTestRuntime() runtime, err := newTestRuntime()
if err != nil { if err != nil {
@ -89,20 +200,73 @@ func TestCommitRun(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer runtime.Destroy(container2) defer runtime.Destroy(container2)
stdout, err := container2.StdoutPipe() stdout, err := container2.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stderr, err := container2.StderrPipe() stderr, err := container2.StderrPipe()
if err != nil {
t.Fatal(err)
}
if err := container2.Start(); err != nil { if err := container2.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
container2.Wait() container2.Wait()
output, err := ioutil.ReadAll(stdout) output, err := ioutil.ReadAll(stdout)
output2, err := ioutil.ReadAll(stderr) if err != nil {
stdout.Close() t.Fatal(err)
stderr.Close()
if string(output) != "hello\n" {
t.Fatalf("\nout: %s\nerr: %s\n", string(output), string(output2))
} }
output2, err := ioutil.ReadAll(stderr)
if err != nil {
t.Fatal(err)
}
if err := stdout.Close(); err != nil {
t.Fatal(err)
}
if err := stderr.Close(); err != nil {
t.Fatal(err)
}
if string(output) != "hello\n" {
t.Fatalf("Unexpected output. Expected %s, received: %s (err: %s)", "hello\n", output, output2)
}
}
func TestStart(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
container, err := runtime.Create(
&Config{
Image: GetTestImage(runtime).Id,
Memory: 33554432,
Cmd: []string{"/bin/cat"},
OpenStdin: true,
},
)
if err != nil {
t.Fatal(err)
}
defer runtime.Destroy(container)
if err := container.Start(); err != nil {
t.Fatal(err)
}
// Give some time to the process to start
container.WaitTimeout(500 * time.Millisecond)
if !container.State.Running {
t.Errorf("Container should be running")
}
if err := container.Start(); err == nil {
t.Fatalf("A running containter should be able to be started")
}
// Try to avoid the timeoout in destroy. Best effort, don't check error
cStdin, _ := container.StdinPipe()
cStdin.Close()
} }
func TestRun(t *testing.T) { func TestRun(t *testing.T) {
@ -208,11 +372,9 @@ func TestExitCode(t *testing.T) {
defer nuke(runtime) defer nuke(runtime)
trueContainer, err := runtime.Create(&Config{ trueContainer, err := runtime.Create(&Config{
Image: GetTestImage(runtime).Id, Image: GetTestImage(runtime).Id,
Cmd: []string{"/bin/true", ""}, Cmd: []string{"/bin/true", ""},
}, })
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -220,12 +382,14 @@ func TestExitCode(t *testing.T) {
if err := trueContainer.Run(); err != nil { if err := trueContainer.Run(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if trueContainer.State.ExitCode != 0 {
t.Errorf("Unexpected exit code %d (expected 0)", trueContainer.State.ExitCode)
}
falseContainer, err := runtime.Create(&Config{ falseContainer, err := runtime.Create(&Config{
Image: GetTestImage(runtime).Id, Image: GetTestImage(runtime).Id,
Cmd: []string{"/bin/false", ""}, Cmd: []string{"/bin/false", ""},
}, })
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -233,13 +397,8 @@ func TestExitCode(t *testing.T) {
if err := falseContainer.Run(); err != nil { if err := falseContainer.Run(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if trueContainer.State.ExitCode != 0 {
t.Errorf("Unexpected exit code %v", trueContainer.State.ExitCode)
}
if falseContainer.State.ExitCode != 1 { if falseContainer.State.ExitCode != 1 {
t.Errorf("Unexpected exit code %v", falseContainer.State.ExitCode) t.Errorf("Unexpected exit code %d (expected 1)", falseContainer.State.ExitCode)
} }
} }
@ -295,32 +454,62 @@ func TestRestartStdin(t *testing.T) {
defer runtime.Destroy(container) defer runtime.Destroy(container)
stdin, err := container.StdinPipe() stdin, err := container.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdout, err := container.StdoutPipe() stdout, err := container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
io.WriteString(stdin, "hello world") if _, err := io.WriteString(stdin, "hello world"); err != nil {
stdin.Close() t.Fatal(err)
}
if err := stdin.Close(); err != nil {
t.Fatal(err)
}
container.Wait() container.Wait()
output, err := ioutil.ReadAll(stdout) output, err := ioutil.ReadAll(stdout)
stdout.Close() if err != nil {
t.Fatal(err)
}
if err := stdout.Close(); err != nil {
t.Fatal(err)
}
if string(output) != "hello world" { if string(output) != "hello world" {
t.Fatal(string(output)) t.Fatalf("Unexpected output. Expected %s, received: %s", "hello world", string(output))
} }
// Restart and try again // Restart and try again
stdin, err = container.StdinPipe() stdin, err = container.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdout, err = container.StdoutPipe() stdout, err = container.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
io.WriteString(stdin, "hello world #2") if _, err := io.WriteString(stdin, "hello world #2"); err != nil {
stdin.Close() t.Fatal(err)
}
if err := stdin.Close(); err != nil {
t.Fatal(err)
}
container.Wait() container.Wait()
output, err = ioutil.ReadAll(stdout) output, err = ioutil.ReadAll(stdout)
stdout.Close() if err != nil {
t.Fatal(err)
}
if err := stdout.Close(); err != nil {
t.Fatal(err)
}
if string(output) != "hello world #2" { if string(output) != "hello world #2" {
t.Fatal(string(output)) t.Fatalf("Unexpected output. Expected %s, received: %s", "hello world #2", string(output))
} }
} }
@ -504,18 +693,31 @@ func TestStdin(t *testing.T) {
defer runtime.Destroy(container) defer runtime.Destroy(container)
stdin, err := container.StdinPipe() stdin, err := container.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdout, err := container.StdoutPipe() stdout, err := container.StdoutPipe()
defer stdin.Close() if err != nil {
defer stdout.Close() t.Fatal(err)
}
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
io.WriteString(stdin, "hello world") defer stdin.Close()
stdin.Close() defer stdout.Close()
if _, err := io.WriteString(stdin, "hello world"); err != nil {
t.Fatal(err)
}
if err := stdin.Close(); err != nil {
t.Fatal(err)
}
container.Wait() container.Wait()
output, err := ioutil.ReadAll(stdout) output, err := ioutil.ReadAll(stdout)
if err != nil {
t.Fatal(err)
}
if string(output) != "hello world" { if string(output) != "hello world" {
t.Fatal(string(output)) t.Fatalf("Unexpected output. Expected %s, received: %s", "hello world", string(output))
} }
} }
@ -538,18 +740,31 @@ func TestTty(t *testing.T) {
defer runtime.Destroy(container) defer runtime.Destroy(container)
stdin, err := container.StdinPipe() stdin, err := container.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdout, err := container.StdoutPipe() stdout, err := container.StdoutPipe()
defer stdin.Close() if err != nil {
defer stdout.Close() t.Fatal(err)
}
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
io.WriteString(stdin, "hello world") defer stdin.Close()
stdin.Close() defer stdout.Close()
if _, err := io.WriteString(stdin, "hello world"); err != nil {
t.Fatal(err)
}
if err := stdin.Close(); err != nil {
t.Fatal(err)
}
container.Wait() container.Wait()
output, err := ioutil.ReadAll(stdout) output, err := ioutil.ReadAll(stdout)
if err != nil {
t.Fatal(err)
}
if string(output) != "hello world" { if string(output) != "hello world" {
t.Fatal(string(output)) t.Fatalf("Unexpected output. Expected %s, received: %s", "hello world", string(output))
} }
} }
@ -568,6 +783,7 @@ func TestEnv(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer runtime.Destroy(container) defer runtime.Destroy(container)
stdout, err := container.StdoutPipe() stdout, err := container.StdoutPipe()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -673,7 +889,7 @@ func BenchmarkRunSequencial(b *testing.B) {
b.Fatal(err) b.Fatal(err)
} }
if string(output) != "foo" { if string(output) != "foo" {
b.Fatalf("Unexecpted output: %v", string(output)) b.Fatalf("Unexpected output: %s", output)
} }
if err := runtime.Destroy(container); err != nil { if err := runtime.Destroy(container); err != nil {
b.Fatal(err) b.Fatal(err)

View File

@ -35,6 +35,5 @@ do
cp -a /dev/$X dev cp -a /dev/$X dev
done done
tar -cf- . | docker put busybox tar -cf- . | docker import - busybox
docker run -i -a -u root busybox /bin/echo Success. docker run -i -u root busybox /bin/echo Success.

View File

@ -51,8 +51,26 @@ documenting your bug report or improvement proposal. If it does, it
never hurts to add a quick "+1" or "I have this problem too". This will never hurts to add a quick "+1" or "I have this problem too". This will
help prioritize the most common problems and requests. help prioritize the most common problems and requests.
Write tests Conventions
~~~~~~~~~~~ ~~~~~~~~~~~
Golang has a great testing suite built in: use it! Take a look at Fork the repo and make changes on your fork in a feature branch:
existing tests for inspiration.
- If it's a bugfix branch, name it XXX-something where XXX is the number of the issue
- If it's a feature branch, create an enhancement issue to announce your intentions, and name it XXX-something where XXX is the number of the issue.
Submit unit tests for your changes. Golang has a great testing suite built
in: use it! Take a look at existing tests for inspiration. Run the full test
suite against your change and the master.
Submit any relevant updates or additions to documentation.
Add clean code:
- Universally formatted code promotes ease of writing, reading, and maintenance. We suggest using gofmt before committing your changes. There's a git pre-commit hook made for doing so.
- curl -o .git/hooks/pre-commit https://raw.github.com/edsrzf/gofmt-git-hook/master/fmt-check && chmod +x .git/hooks/pre-commit
Pull requests descriptions should be as clear as possible and include a
referenced to all the issues that they address.
Add your name to the AUTHORS file.

View File

@ -10,10 +10,14 @@ import (
"time" "time"
) )
// A Graph is a store for versioned filesystem images and the relationship between them.
type Graph struct { type Graph struct {
Root string Root string
idIndex *TruncIndex
} }
// NewGraph instantiates a new graph at the given root path in the filesystem.
// `root` will be created if it doesn't exist.
func NewGraph(root string) (*Graph, error) { func NewGraph(root string) (*Graph, error) {
abspath, err := filepath.Abs(root) abspath, err := filepath.Abs(root)
if err != nil { if err != nil {
@ -23,9 +27,26 @@ func NewGraph(root string) (*Graph, error) {
if err := os.Mkdir(root, 0700); err != nil && !os.IsExist(err) { if err := os.Mkdir(root, 0700); err != nil && !os.IsExist(err) {
return nil, err return nil, err
} }
return &Graph{ graph := &Graph{
Root: abspath, Root: abspath,
}, nil idIndex: NewTruncIndex(),
}
if err := graph.restore(); err != nil {
return nil, err
}
return graph, nil
}
func (graph *Graph) restore() error {
dir, err := ioutil.ReadDir(graph.Root)
if err != nil {
return err
}
for _, v := range dir {
id := v.Name()
graph.idIndex.Add(id)
}
return nil
} }
// FIXME: Implement error subclass instead of looking at the error text // FIXME: Implement error subclass instead of looking at the error text
@ -34,6 +55,8 @@ func (graph *Graph) IsNotExist(err error) bool {
return err != nil && strings.Contains(err.Error(), "does not exist") return err != nil && strings.Contains(err.Error(), "does not exist")
} }
// Exists returns true if an image is registered at the given id.
// If the image doesn't exist or if an error is encountered, false is returned.
func (graph *Graph) Exists(id string) bool { func (graph *Graph) Exists(id string) bool {
if _, err := graph.Get(id); err != nil { if _, err := graph.Get(id); err != nil {
return false return false
@ -41,7 +64,12 @@ func (graph *Graph) Exists(id string) bool {
return true return true
} }
func (graph *Graph) Get(id string) (*Image, error) { // Get returns the image with the given id, or an error if the image doesn't exist.
func (graph *Graph) Get(name string) (*Image, error) {
id, err := graph.idIndex.Get(name)
if err != nil {
return nil, err
}
// FIXME: return nil when the image doesn't exist, instead of an error // FIXME: return nil when the image doesn't exist, instead of an error
img, err := LoadImage(graph.imageRoot(id)) img, err := LoadImage(graph.imageRoot(id))
if err != nil { if err != nil {
@ -54,6 +82,7 @@ func (graph *Graph) Get(id string) (*Image, error) {
return img, nil return img, nil
} }
// Create creates a new image and registers it in the graph.
func (graph *Graph) Create(layerData Archive, container *Container, comment string) (*Image, error) { func (graph *Graph) Create(layerData Archive, container *Container, comment string) (*Image, error) {
img := &Image{ img := &Image{
Id: GenerateId(), Id: GenerateId(),
@ -71,6 +100,8 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment stri
return img, nil return img, nil
} }
// Register imports a pre-existing image into the graph.
// FIXME: pass img as first argument
func (graph *Graph) Register(layerData Archive, img *Image) error { func (graph *Graph) Register(layerData Archive, img *Image) error {
if err := ValidateId(img.Id); err != nil { if err := ValidateId(img.Id); err != nil {
return err return err
@ -92,9 +123,11 @@ func (graph *Graph) Register(layerData Archive, img *Image) error {
return err return err
} }
img.graph = graph img.graph = graph
graph.idIndex.Add(img.Id)
return nil return nil
} }
// Mktemp creates a temporary sub-directory inside the graph's filesystem.
func (graph *Graph) Mktemp(id string) (string, error) { func (graph *Graph) Mktemp(id string) (string, error) {
tmp, err := NewGraph(path.Join(graph.Root, ":tmp:")) tmp, err := NewGraph(path.Join(graph.Root, ":tmp:"))
if err != nil { if err != nil {
@ -106,12 +139,15 @@ func (graph *Graph) Mktemp(id string) (string, error) {
return tmp.imageRoot(id), nil return tmp.imageRoot(id), nil
} }
// Garbage returns the "garbage", a staging area for deleted images.
// This allows images to be deleted atomically by os.Rename(), instead of
// os.RemoveAll() which is prone to race conditions.
func (graph *Graph) Garbage() (*Graph, error) { func (graph *Graph) Garbage() (*Graph, error) {
return NewGraph(path.Join(graph.Root, ":garbage:")) return NewGraph(path.Join(graph.Root, ":garbage:"))
} }
// Check if given error is "not empty" // Check if given error is "not empty".
// Note: this is the way golang do it internally with os.IsNotExists // Note: this is the way golang does it internally with os.IsNotExists.
func isNotEmpty(err error) bool { func isNotEmpty(err error) bool {
switch pe := err.(type) { switch pe := err.(type) {
case nil: case nil:
@ -124,13 +160,21 @@ func isNotEmpty(err error) bool {
return strings.Contains(err.Error(), " not empty") return strings.Contains(err.Error(), " not empty")
} }
func (graph *Graph) Delete(id string) error { // Delete atomically removes an image from the graph.
func (graph *Graph) Delete(name string) error {
id, err := graph.idIndex.Get(name)
if err != nil {
return err
}
garbage, err := graph.Garbage() garbage, err := graph.Garbage()
if err != nil { if err != nil {
return err return err
} }
graph.idIndex.Delete(id)
err = os.Rename(graph.imageRoot(id), garbage.imageRoot(id)) err = os.Rename(graph.imageRoot(id), garbage.imageRoot(id))
if err != nil { if err != nil {
// FIXME: this introduces a race condition in Delete() if the image is already present
// in garbage. Let's store at random names in grabage instead.
if isNotEmpty(err) { if isNotEmpty(err) {
Debugf("The image %s is already present in garbage. Removing it.", id) Debugf("The image %s is already present in garbage. Removing it.", id)
if err = os.RemoveAll(garbage.imageRoot(id)); err != nil { if err = os.RemoveAll(garbage.imageRoot(id)); err != nil {
@ -150,14 +194,20 @@ func (graph *Graph) Delete(id string) error {
return nil return nil
} }
// Undelete moves an image back from the garbage to the main graph.
func (graph *Graph) Undelete(id string) error { func (graph *Graph) Undelete(id string) error {
garbage, err := graph.Garbage() garbage, err := graph.Garbage()
if err != nil { if err != nil {
return err return err
} }
return os.Rename(garbage.imageRoot(id), graph.imageRoot(id)) if err := os.Rename(garbage.imageRoot(id), graph.imageRoot(id)); err != nil {
return err
}
graph.idIndex.Add(id)
return nil
} }
// GarbageCollect definitely deletes all images moved to the garbage.
func (graph *Graph) GarbageCollect() error { func (graph *Graph) GarbageCollect() error {
garbage, err := graph.Garbage() garbage, err := graph.Garbage()
if err != nil { if err != nil {
@ -166,6 +216,7 @@ func (graph *Graph) GarbageCollect() error {
return os.RemoveAll(garbage.Root) return os.RemoveAll(garbage.Root)
} }
// Map returns a list of all images in the graph, addressable by ID.
func (graph *Graph) Map() (map[string]*Image, error) { func (graph *Graph) Map() (map[string]*Image, error) {
// FIXME: this should replace All() // FIXME: this should replace All()
all, err := graph.All() all, err := graph.All()
@ -179,6 +230,7 @@ func (graph *Graph) Map() (map[string]*Image, error) {
return images, nil return images, nil
} }
// All returns a list of all images in the graph.
func (graph *Graph) All() ([]*Image, error) { func (graph *Graph) All() ([]*Image, error) {
var images []*Image var images []*Image
err := graph.WalkAll(func(image *Image) { err := graph.WalkAll(func(image *Image) {
@ -187,6 +239,8 @@ func (graph *Graph) All() ([]*Image, error) {
return images, err return images, err
} }
// WalkAll iterates over each image in the graph, and passes it to a handler.
// The walking order is undetermined.
func (graph *Graph) WalkAll(handler func(*Image)) error { func (graph *Graph) WalkAll(handler func(*Image)) error {
files, err := ioutil.ReadDir(graph.Root) files, err := ioutil.ReadDir(graph.Root)
if err != nil { if err != nil {
@ -203,6 +257,10 @@ func (graph *Graph) WalkAll(handler func(*Image)) error {
return nil return nil
} }
// ByParent returns a lookup table of images by their parent.
// If an image of id ID has 3 children images, then the value for key ID
// will be a list of 3 images.
// If an image has no children, it will not have an entry in the table.
func (graph *Graph) ByParent() (map[string][]*Image, error) { func (graph *Graph) ByParent() (map[string][]*Image, error) {
byParent := make(map[string][]*Image) byParent := make(map[string][]*Image)
err := graph.WalkAll(func(image *Image) { err := graph.WalkAll(func(image *Image) {
@ -219,6 +277,8 @@ func (graph *Graph) ByParent() (map[string][]*Image, error) {
return byParent, err return byParent, err
} }
// Heads returns all heads in the graph, keyed by id.
// A head is an image which is not the parent of another image in the graph.
func (graph *Graph) Heads() (map[string]*Image, error) { func (graph *Graph) Heads() (map[string]*Image, error) {
heads := make(map[string]*Image) heads := make(map[string]*Image)
byParent, err := graph.ByParent() byParent, err := graph.ByParent()

View File

@ -120,6 +120,29 @@ func TestMount(t *testing.T) {
}() }()
} }
// Test that an image can be deleted by its shorthand prefix
func TestDeletePrefix(t *testing.T) {
graph := tempGraph(t)
defer os.RemoveAll(graph.Root)
img := createTestImage(graph, t)
if err := graph.Delete(TruncateId(img.Id)); err != nil {
t.Fatal(err)
}
assertNImages(graph, t, 0)
}
func createTestImage(graph *Graph, t *testing.T) *Image {
archive, err := fakeTar()
if err != nil {
t.Fatal(err)
}
img, err := graph.Create(archive, nil, "Test image")
if err != nil {
t.Fatal(err)
}
return img
}
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
graph := tempGraph(t) graph := tempGraph(t)
defer os.RemoveAll(graph.Root) defer os.RemoveAll(graph.Root)

View File

@ -150,6 +150,10 @@ func (image *Image) Changes(rw string) ([]Change, error) {
return Changes(layers, rw) return Changes(layers, rw)
} }
func (image *Image) ShortId() string {
return TruncateId(image.Id)
}
func ValidateId(id string) error { func ValidateId(id string) error {
if id == "" { if id == "" {
return fmt.Errorf("Image id can't be empty") return fmt.Errorf("Image id can't be empty")

View File

@ -1,7 +1,6 @@
package docker package docker
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
@ -30,40 +29,25 @@ func networkRange(network *net.IPNet) (net.IP, net.IP) {
} }
// Converts a 4 bytes IP into a 32 bit integer // Converts a 4 bytes IP into a 32 bit integer
func ipToInt(ip net.IP) (int32, error) { func ipToInt(ip net.IP) int32 {
buf := bytes.NewBuffer(ip.To4()) return int32(binary.BigEndian.Uint32(ip.To4()))
var n int32
if err := binary.Read(buf, binary.BigEndian, &n); err != nil {
return 0, err
}
return n, nil
} }
// Converts 32 bit integer into a 4 bytes IP address // Converts 32 bit integer into a 4 bytes IP address
func intToIp(n int32) (net.IP, error) { func intToIp(n int32) net.IP {
var buf bytes.Buffer b := make([]byte, 4)
if err := binary.Write(&buf, binary.BigEndian, &n); err != nil { binary.BigEndian.PutUint32(b, uint32(n))
return net.IP{}, err return net.IP(b)
}
ip := net.IPv4(0, 0, 0, 0).To4()
for i := 0; i < net.IPv4len; i++ {
ip[i] = buf.Bytes()[i]
}
return ip, nil
} }
// Given a netmask, calculates the number of available hosts // Given a netmask, calculates the number of available hosts
func networkSize(mask net.IPMask) (int32, error) { func networkSize(mask net.IPMask) int32 {
m := net.IPv4Mask(0, 0, 0, 0) m := net.IPv4Mask(0, 0, 0, 0)
for i := 0; i < net.IPv4len; i++ { for i := 0; i < net.IPv4len; i++ {
m[i] = ^mask[i] m[i] = ^mask[i]
} }
buf := bytes.NewBuffer(m)
var n int32 return int32(binary.BigEndian.Uint32(m)) + 1
if err := binary.Read(buf, binary.BigEndian, &n); err != nil {
return 0, err
}
return n + 1, nil
} }
// Wrapper around the iptables command // Wrapper around the iptables command
@ -212,65 +196,96 @@ func newPortAllocator(start, end int) (*PortAllocator, error) {
// IP allocator: Atomatically allocate and release networking ports // IP allocator: Atomatically allocate and release networking ports
type IPAllocator struct { type IPAllocator struct {
network *net.IPNet network *net.IPNet
queue chan (net.IP) queueAlloc chan allocatedIP
queueReleased chan net.IP
inUse map[int32]struct{}
} }
func (alloc *IPAllocator) populate() error { type allocatedIP struct {
ip net.IP
err error
}
func (alloc *IPAllocator) run() {
firstIP, _ := networkRange(alloc.network) firstIP, _ := networkRange(alloc.network)
size, err := networkSize(alloc.network.Mask) ipNum := ipToInt(firstIP)
if err != nil { ownIP := ipToInt(alloc.network.IP)
return err size := networkSize(alloc.network.Mask)
}
// The queue size should be the network size - 3 pos := int32(1)
// -1 for the network address, -1 for the broadcast address and max := size - 2 // -1 for the broadcast address, -1 for the gateway address
// -1 for the gateway address for {
alloc.queue = make(chan net.IP, size-3) var (
for i := int32(1); i < size-1; i++ { newNum int32
ipNum, err := ipToInt(firstIP) inUse bool
if err != nil { )
return err
} // Find first unused IP, give up after one whole round
ip, err := intToIp(ipNum + int32(i)) for attempt := int32(0); attempt < max; attempt++ {
if err != nil { newNum = ipNum + pos
return err
} pos = pos%max + 1
// Discard the network IP (that's the host IP address)
if ip.Equal(alloc.network.IP) { // The network's IP is never okay to use
if newNum == ownIP {
continue continue
} }
alloc.queue <- ip
if _, inUse = alloc.inUse[newNum]; !inUse {
// We found an unused IP
break
}
}
ip := allocatedIP{ip: intToIp(newNum)}
if inUse {
ip.err = errors.New("No unallocated IP available")
}
select {
case alloc.queueAlloc <- ip:
alloc.inUse[newNum] = struct{}{}
case released := <-alloc.queueReleased:
r := ipToInt(released)
delete(alloc.inUse, r)
if inUse {
// If we couldn't allocate a new IP, the released one
// will be the only free one now, so instantly use it
// next time
pos = r - ipNum
} else {
// Use same IP as last time
if pos == 1 {
pos = max
} else {
pos--
}
}
}
} }
return nil
} }
func (alloc *IPAllocator) Acquire() (net.IP, error) { func (alloc *IPAllocator) Acquire() (net.IP, error) {
select { ip := <-alloc.queueAlloc
case ip := <-alloc.queue: return ip.ip, ip.err
return ip, nil
default:
return net.IP{}, errors.New("No more IP addresses available")
}
return net.IP{}, nil
} }
func (alloc *IPAllocator) Release(ip net.IP) error { func (alloc *IPAllocator) Release(ip net.IP) {
select { alloc.queueReleased <- ip
case alloc.queue <- ip:
return nil
default:
return errors.New("Too many IP addresses have been released")
}
return nil
} }
func newIPAllocator(network *net.IPNet) (*IPAllocator, error) { func newIPAllocator(network *net.IPNet) *IPAllocator {
alloc := &IPAllocator{ alloc := &IPAllocator{
network: network, network: network,
queueAlloc: make(chan allocatedIP),
queueReleased: make(chan net.IP),
inUse: make(map[int32]struct{}),
} }
if err := alloc.populate(); err != nil {
return nil, err go alloc.run()
}
return alloc, nil return alloc
} }
// Network interface represents the networking stack of a container // Network interface represents the networking stack of a container
@ -297,7 +312,7 @@ func (iface *NetworkInterface) AllocatePort(port int) (int, error) {
} }
// Release: Network cleanup - release all resources // Release: Network cleanup - release all resources
func (iface *NetworkInterface) Release() error { func (iface *NetworkInterface) Release() {
for _, port := range iface.extPorts { for _, port := range iface.extPorts {
if err := iface.manager.portMapper.Unmap(port); err != nil { if err := iface.manager.portMapper.Unmap(port); err != nil {
log.Printf("Unable to unmap port %v: %v", port, err) log.Printf("Unable to unmap port %v: %v", port, err)
@ -307,7 +322,8 @@ func (iface *NetworkInterface) Release() error {
} }
} }
return iface.manager.ipAllocator.Release(iface.IPNet.IP)
iface.manager.ipAllocator.Release(iface.IPNet.IP)
} }
// Network Manager manages a set of network interfaces // Network Manager manages a set of network interfaces
@ -342,10 +358,7 @@ func newNetworkManager(bridgeIface string) (*NetworkManager, error) {
} }
network := addr.(*net.IPNet) network := addr.(*net.IPNet)
ipAllocator, err := newIPAllocator(network) ipAllocator := newIPAllocator(network)
if err != nil {
return nil, err
}
portAllocator, err := newPortAllocator(portRangeStart, portRangeEnd) portAllocator, err := newPortAllocator(portRangeStart, portRangeEnd)
if err != nil { if err != nil {

View File

@ -28,8 +28,8 @@ func TestNetworkRange(t *testing.T) {
if !last.Equal(net.ParseIP("192.168.0.255")) { if !last.Equal(net.ParseIP("192.168.0.255")) {
t.Error(last.String()) t.Error(last.String())
} }
if size, err := networkSize(network.Mask); err != nil || size != 256 { if size := networkSize(network.Mask); size != 256 {
t.Error(size, err) t.Error(size)
} }
// Class A test // Class A test
@ -41,8 +41,8 @@ func TestNetworkRange(t *testing.T) {
if !last.Equal(net.ParseIP("10.255.255.255")) { if !last.Equal(net.ParseIP("10.255.255.255")) {
t.Error(last.String()) t.Error(last.String())
} }
if size, err := networkSize(network.Mask); err != nil || size != 16777216 { if size := networkSize(network.Mask); size != 16777216 {
t.Error(size, err) t.Error(size)
} }
// Class A, random IP address // Class A, random IP address
@ -64,8 +64,8 @@ func TestNetworkRange(t *testing.T) {
if !last.Equal(net.ParseIP("10.1.2.3")) { if !last.Equal(net.ParseIP("10.1.2.3")) {
t.Error(last.String()) t.Error(last.String())
} }
if size, err := networkSize(network.Mask); err != nil || size != 1 { if size := networkSize(network.Mask); size != 1 {
t.Error(size, err) t.Error(size)
} }
// 31bit mask // 31bit mask
@ -77,8 +77,8 @@ func TestNetworkRange(t *testing.T) {
if !last.Equal(net.ParseIP("10.1.2.3")) { if !last.Equal(net.ParseIP("10.1.2.3")) {
t.Error(last.String()) t.Error(last.String())
} }
if size, err := networkSize(network.Mask); err != nil || size != 2 { if size := networkSize(network.Mask); size != 2 {
t.Error(size, err) t.Error(size)
} }
// 26bit mask // 26bit mask
@ -90,54 +90,130 @@ func TestNetworkRange(t *testing.T) {
if !last.Equal(net.ParseIP("10.1.2.63")) { if !last.Equal(net.ParseIP("10.1.2.63")) {
t.Error(last.String()) t.Error(last.String())
} }
if size, err := networkSize(network.Mask); err != nil || size != 64 { if size := networkSize(network.Mask); size != 64 {
t.Error(size, err) t.Error(size)
} }
} }
func TestConversion(t *testing.T) { func TestConversion(t *testing.T) {
ip := net.ParseIP("127.0.0.1") ip := net.ParseIP("127.0.0.1")
i, err := ipToInt(ip) i := ipToInt(ip)
if err != nil {
t.Fatal(err)
}
if i == 0 { if i == 0 {
t.Fatal("converted to zero") t.Fatal("converted to zero")
} }
conv, err := intToIp(i) conv := intToIp(i)
if err != nil {
t.Fatal(err)
}
if !ip.Equal(conv) { if !ip.Equal(conv) {
t.Error(conv.String()) t.Error(conv.String())
} }
} }
func TestIPAllocator(t *testing.T) { func TestIPAllocator(t *testing.T) {
gwIP, n, _ := net.ParseCIDR("127.0.0.1/29") expectedIPs := []net.IP{
alloc, err := newIPAllocator(&net.IPNet{IP: gwIP, Mask: n.Mask}) 0: net.IPv4(127, 0, 0, 2),
if err != nil { 1: net.IPv4(127, 0, 0, 3),
t.Fatal(err) 2: net.IPv4(127, 0, 0, 4),
3: net.IPv4(127, 0, 0, 5),
4: net.IPv4(127, 0, 0, 6),
} }
var lastIP net.IP
gwIP, n, _ := net.ParseCIDR("127.0.0.1/29")
alloc := newIPAllocator(&net.IPNet{IP: gwIP, Mask: n.Mask})
// Pool after initialisation (f = free, u = used)
// 2(f) - 3(f) - 4(f) - 5(f) - 6(f)
// ↑
// Check that we get 5 IPs, from 127.0.0.2127.0.0.6, in that
// order.
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
ip, err := alloc.Acquire() ip, err := alloc.Acquire()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
lastIP = ip
assertIPEquals(t, expectedIPs[i], ip)
} }
ip, err := alloc.Acquire() // Before loop begin
// 2(f) - 3(f) - 4(f) - 5(f) - 6(f)
// ↑
// After i = 0
// 2(u) - 3(f) - 4(f) - 5(f) - 6(f)
// ↑
// After i = 1
// 2(u) - 3(u) - 4(f) - 5(f) - 6(f)
// ↑
// After i = 2
// 2(u) - 3(u) - 4(u) - 5(f) - 6(f)
// ↑
// After i = 3
// 2(u) - 3(u) - 4(u) - 5(u) - 6(f)
// ↑
// After i = 4
// 2(u) - 3(u) - 4(u) - 5(u) - 6(u)
// ↑
// Check that there are no more IPs
_, err := alloc.Acquire()
if err == nil { if err == nil {
t.Fatal("There shouldn't be any IP addresses at this point") t.Fatal("There shouldn't be any IP addresses at this point")
} }
// Release 1 IP
alloc.Release(lastIP) // Release some IPs in non-sequential order
ip, err = alloc.Acquire() alloc.Release(expectedIPs[3])
// 2(u) - 3(u) - 4(u) - 5(f) - 6(u)
// ↑
alloc.Release(expectedIPs[2])
// 2(u) - 3(u) - 4(f) - 5(f) - 6(u)
// ↑
alloc.Release(expectedIPs[4])
// 2(u) - 3(u) - 4(f) - 5(f) - 6(f)
// ↑
// Make sure that IPs are reused in sequential order, starting
// with the first released IP
newIPs := make([]net.IP, 3)
for i := 0; i < 3; i++ {
ip, err := alloc.Acquire()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !ip.Equal(lastIP) {
t.Fatal(ip.String()) newIPs[i] = ip
}
// Before loop begin
// 2(u) - 3(u) - 4(f) - 5(f) - 6(f)
// ↑
// After i = 0
// 2(u) - 3(u) - 4(f) - 5(u) - 6(f)
// ↑
// After i = 1
// 2(u) - 3(u) - 4(f) - 5(u) - 6(u)
// ↑
// After i = 2
// 2(u) - 3(u) - 4(u) - 5(u) - 6(u)
// ↑
assertIPEquals(t, expectedIPs[3], newIPs[0])
assertIPEquals(t, expectedIPs[4], newIPs[1])
assertIPEquals(t, expectedIPs[2], newIPs[2])
_, err = alloc.Acquire()
if err == nil {
t.Fatal("There shouldn't be any IP addresses at this point")
}
}
func assertIPEquals(t *testing.T, ip1, ip2 net.IP) {
if !ip1.Equal(ip2) {
t.Fatalf("Expected IP %s, got %s", ip1, ip2)
} }
} }

View File

@ -20,7 +20,7 @@ func ListenAndServeHTTP(addr string, service Service) error {
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
cmd, args := URLToCall(r.URL) cmd, args := URLToCall(r.URL)
if err := call(service, r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil { if err := call(service, r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil {
fmt.Fprintf(w, "Error: "+err.Error()+"\n") fmt.Fprintln(w, "Error:", err.Error())
} }
})) }))
} }

View File

@ -51,8 +51,8 @@ func ListenAndServe(proto, addr string, service Service) error {
CLIENT_SOCKET = conn CLIENT_SOCKET = conn
} }
if err := Serve(conn, service); err != nil { if err := Serve(conn, service); err != nil {
log.Printf("Error: " + err.Error() + "\n") log.Println("Error:", err.Error())
fmt.Fprintf(conn, "Error: "+err.Error()+"\n") fmt.Fprintln(conn, "Error:", err.Error())
} }
conn.Close() conn.Close()
}() }()

View File

@ -1,6 +1,7 @@
package docker package docker
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/dotcloud/docker/auth" "github.com/dotcloud/docker/auth"
@ -20,7 +21,7 @@ func NewImgJson(src []byte) (*Image, error) {
ret := &Image{} ret := &Image{}
Debugf("Json string: {%s}\n", src) Debugf("Json string: {%s}\n", src)
// FIXME: Is there a cleaner way to "puryfy" the input json? // FIXME: Is there a cleaner way to "purify" the input json?
if err := json.Unmarshal(src, ret); err != nil { if err := json.Unmarshal(src, ret); err != nil {
return nil, err return nil, err
} }
@ -32,7 +33,7 @@ func NewImgJson(src []byte) (*Image, error) {
func NewMultipleImgJson(src []byte) ([]*Image, error) { func NewMultipleImgJson(src []byte) ([]*Image, error) {
ret := []*Image{} ret := []*Image{}
dec := json.NewDecoder(strings.NewReader(string(src))) dec := json.NewDecoder(bytes.NewReader(src))
for { for {
m := &Image{} m := &Image{}
if err := dec.Decode(m); err == io.EOF { if err := dec.Decode(m); err == io.EOF {
@ -135,7 +136,7 @@ func (graph *Graph) getRemoteImage(stdout io.Writer, imgId string, authConfig *a
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return img, res.Body, nil return img, ProgressReader(res.Body, int(res.ContentLength), stdout), nil
} }
func (graph *Graph) PullImage(stdout io.Writer, imgId string, authConfig *auth.AuthConfig) error { func (graph *Graph) PullImage(stdout io.Writer, imgId string, authConfig *auth.AuthConfig) error {
@ -183,10 +184,10 @@ func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, re
if err != nil { if err != nil {
return err return err
} }
defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return fmt.Errorf("HTTP code: %d", res.StatusCode) return fmt.Errorf("HTTP code: %d", res.StatusCode)
} }
defer res.Body.Close()
rawJson, err := ioutil.ReadAll(res.Body) rawJson, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
return err return err
@ -226,7 +227,7 @@ func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, authConfig *auth
fmt.Fprintf(stdout, "Pushing %s metadata\n", img.Id) fmt.Fprintf(stdout, "Pushing %s metadata\n", img.Id)
// FIXME: try json with UTF8 // FIXME: try json with UTF8
jsonData := strings.NewReader(string(jsonRaw)) jsonData := bytes.NewReader(jsonRaw)
req, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/images/"+img.Id+"/json", jsonData) req, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/images/"+img.Id+"/json", jsonData)
if err != nil { if err != nil {
return err return err
@ -237,6 +238,7 @@ func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, authConfig *auth
if err != nil { if err != nil {
return fmt.Errorf("Failed to upload metadata: %s", err) return fmt.Errorf("Failed to upload metadata: %s", err)
} }
defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
switch res.StatusCode { switch res.StatusCode {
case 204: case 204:
@ -256,9 +258,13 @@ func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, authConfig *auth
req2, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/images/"+img.Id+"/layer", nil) req2, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/images/"+img.Id+"/layer", nil)
req2.SetBasicAuth(authConfig.Username, authConfig.Password) req2.SetBasicAuth(authConfig.Username, authConfig.Password)
res2, err := client.Do(req2) res2, err := client.Do(req2)
if err != nil || res2.StatusCode != 307 { if err != nil {
return fmt.Errorf("Registry returned error: %s", err) return fmt.Errorf("Registry returned error: %s", err)
} }
res2.Body.Close()
if res2.StatusCode != 307 {
return fmt.Errorf("Registry returned unexpected HTTP status code %d, expected 307", res2.StatusCode)
}
url, err := res2.Location() url, err := res2.Location()
if err != nil || url == nil { if err != nil || url == nil {
return fmt.Errorf("Failed to retrieve layer upload location: %s", err) return fmt.Errorf("Failed to retrieve layer upload location: %s", err)
@ -267,25 +273,28 @@ func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, authConfig *auth
// FIXME: Don't do this :D. Check the S3 requierement and implement chunks of 5MB // FIXME: Don't do this :D. Check the S3 requierement and implement chunks of 5MB
// FIXME2: I won't stress it enough, DON'T DO THIS! very high priority // FIXME2: I won't stress it enough, DON'T DO THIS! very high priority
layerData2, err := Tar(path.Join(graph.Root, img.Id, "layer"), Gzip) layerData2, err := Tar(path.Join(graph.Root, img.Id, "layer"), Gzip)
layerData, err := Tar(path.Join(graph.Root, img.Id, "layer"), Gzip)
if err != nil {
return fmt.Errorf("Failed to generate layer archive: %s", err)
}
req3, err := http.NewRequest("PUT", url.String(), layerData)
if err != nil {
return err
}
tmp, err := ioutil.ReadAll(layerData2) tmp, err := ioutil.ReadAll(layerData2)
if err != nil { if err != nil {
return err return err
} }
req3.ContentLength = int64(len(tmp)) layerLength := len(tmp)
layerData, err := Tar(path.Join(graph.Root, img.Id, "layer"), Gzip)
if err != nil {
return fmt.Errorf("Failed to generate layer archive: %s", err)
}
req3, err := http.NewRequest("PUT", url.String(), ProgressReader(layerData.(io.ReadCloser), layerLength, stdout))
if err != nil {
return err
}
req3.ContentLength = int64(layerLength)
req3.TransferEncoding = []string{"none"} req3.TransferEncoding = []string{"none"}
res3, err := client.Do(req3) res3, err := client.Do(req3)
if err != nil { if err != nil {
return fmt.Errorf("Failed to upload layer: %s", err) return fmt.Errorf("Failed to upload layer: %s", err)
} }
res3.Body.Close()
if res3.StatusCode != 200 { if res3.StatusCode != 200 {
return fmt.Errorf("Received HTTP code %d while uploading layer", res3.StatusCode) return fmt.Errorf("Received HTTP code %d while uploading layer", res3.StatusCode)
} }
@ -315,12 +324,13 @@ func (graph *Graph) pushTag(remote, revision, tag string, authConfig *auth.AuthC
req.Header.Add("Content-type", "application/json") req.Header.Add("Content-type", "application/json")
req.SetBasicAuth(authConfig.Username, authConfig.Password) req.SetBasicAuth(authConfig.Username, authConfig.Password)
res, err := client.Do(req) res, err := client.Do(req)
if err != nil || (res.StatusCode != 200 && res.StatusCode != 201) { if err != nil {
if res != nil {
return fmt.Errorf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote)
}
return err return err
} }
res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 {
return fmt.Errorf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote)
}
Debugf("Result of push tag: %d\n", res.StatusCode) Debugf("Result of push tag: %d\n", res.StatusCode)
switch res.StatusCode { switch res.StatusCode {
default: default:

View File

@ -21,6 +21,7 @@ type Runtime struct {
graph *Graph graph *Graph
repositories *TagStore repositories *TagStore
authConfig *auth.AuthConfig authConfig *auth.AuthConfig
idIndex *TruncIndex
} }
var sysInitPath string var sysInitPath string
@ -47,7 +48,11 @@ func (runtime *Runtime) getContainerElement(id string) *list.Element {
return nil return nil
} }
func (runtime *Runtime) Get(id string) *Container { func (runtime *Runtime) Get(name string) *Container {
id, err := runtime.idIndex.Get(name)
if err != nil {
return nil
}
e := runtime.getContainerElement(id) e := runtime.getContainerElement(id)
if e == nil { if e == nil {
return nil return nil
@ -72,6 +77,7 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) {
// Generate id // Generate id
id := GenerateId() id := GenerateId()
// Generate default hostname // Generate default hostname
// FIXME: the lxc template no longer needs to set a default hostname
if config.Hostname == "" { if config.Hostname == "" {
config.Hostname = id[:12] config.Hostname = id[:12]
} }
@ -140,15 +146,9 @@ func (runtime *Runtime) Register(container *Container) error {
} else { } else {
container.stdinPipe = NopWriteCloser(ioutil.Discard) // Silently drop stdin container.stdinPipe = NopWriteCloser(ioutil.Discard) // Silently drop stdin
} }
// Setup logging of stdout and stderr to disk
if err := runtime.LogToDisk(container.stdout, container.logPath("stdout")); err != nil {
return err
}
if err := runtime.LogToDisk(container.stderr, container.logPath("stderr")); err != nil {
return err
}
// done // done
runtime.containers.PushBack(container) runtime.containers.PushBack(container)
runtime.idIndex.Add(container.Id)
return nil return nil
} }
@ -157,7 +157,7 @@ func (runtime *Runtime) LogToDisk(src *writeBroadcaster, dst string) error {
if err != nil { if err != nil {
return err return err
} }
src.AddWriter(NopWriteCloser(log)) src.AddWriter(log)
return nil return nil
} }
@ -178,6 +178,7 @@ func (runtime *Runtime) Destroy(container *Container) error {
} }
} }
// Deregister the container before removing its directory, to avoid race conditions // Deregister the container before removing its directory, to avoid race conditions
runtime.idIndex.Delete(container.Id)
runtime.containers.Remove(element) runtime.containers.Remove(element)
if err := os.RemoveAll(container.root); err != nil { if err := os.RemoveAll(container.root); err != nil {
return fmt.Errorf("Unable to remove filesystem for %v: %v", container.Id, err) return fmt.Errorf("Unable to remove filesystem for %v: %v", container.Id, err)
@ -229,6 +230,7 @@ func (runtime *Runtime) restore() error {
return nil return nil
} }
// FIXME: harmonize with NewGraph()
func NewRuntime() (*Runtime, error) { func NewRuntime() (*Runtime, error) {
return NewRuntimeFromDirectory("/var/lib/docker") return NewRuntimeFromDirectory("/var/lib/docker")
} }
@ -266,6 +268,7 @@ func NewRuntimeFromDirectory(root string) (*Runtime, error) {
graph: g, graph: g,
repositories: repositories, repositories: repositories,
authConfig: authConfig, authConfig: authConfig,
idIndex: NewTruncIndex(),
} }
if err := runtime.restore(); err != nil { if err := runtime.restore(); err != nil {

View File

@ -106,7 +106,7 @@ func (store *TagStore) ImageName(id string) string {
if names, exists := store.ById()[id]; exists && len(names) > 0 { if names, exists := store.ById()[id]; exists && len(names) > 0 {
return names[0] return names[0]
} }
return id return TruncateId(id)
} }
func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {

View File

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/rcli"
"index/suffixarray"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@ -220,6 +222,7 @@ func (w *writeBroadcaster) AddWriter(writer io.WriteCloser) {
w.writers.PushBack(writer) w.writers.PushBack(writer)
} }
// FIXME: Is that function used?
func (w *writeBroadcaster) RemoveWriter(writer io.WriteCloser) { func (w *writeBroadcaster) RemoveWriter(writer io.WriteCloser) {
for e := w.writers.Front(); e != nil; e = e.Next() { for e := w.writers.Front(); e != nil; e = e.Next() {
v := e.Value.(io.Writer) v := e.Value.(io.Writer)
@ -252,9 +255,94 @@ func (w *writeBroadcaster) Close() error {
writer := e.Value.(io.WriteCloser) writer := e.Value.(io.WriteCloser)
writer.Close() writer.Close()
} }
w.writers.Init()
return nil return nil
} }
func newWriteBroadcaster() *writeBroadcaster { func newWriteBroadcaster() *writeBroadcaster {
return &writeBroadcaster{list.New()} return &writeBroadcaster{list.New()}
} }
func getTotalUsedFds() int {
if fds, err := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", os.Getpid())); err != nil {
Debugf("Error opening /proc/%d/fd: %s", os.Getpid(), err)
} else {
return len(fds)
}
return -1
}
// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes.
// This is used to retrieve image and container IDs by more convenient shorthand prefixes.
type TruncIndex struct {
index *suffixarray.Index
ids map[string]bool
bytes []byte
}
func NewTruncIndex() *TruncIndex {
return &TruncIndex{
index: suffixarray.New([]byte{' '}),
ids: make(map[string]bool),
bytes: []byte{' '},
}
}
func (idx *TruncIndex) Add(id string) error {
if strings.Contains(id, " ") {
return fmt.Errorf("Illegal character: ' '")
}
if _, exists := idx.ids[id]; exists {
return fmt.Errorf("Id already exists: %s", id)
}
idx.ids[id] = true
idx.bytes = append(idx.bytes, []byte(id+" ")...)
idx.index = suffixarray.New(idx.bytes)
return nil
}
func (idx *TruncIndex) Delete(id string) error {
if _, exists := idx.ids[id]; !exists {
return fmt.Errorf("No such id: %s", id)
}
before, after, err := idx.lookup(id)
if err != nil {
return err
}
delete(idx.ids, id)
idx.bytes = append(idx.bytes[:before], idx.bytes[after:]...)
idx.index = suffixarray.New(idx.bytes)
return nil
}
func (idx *TruncIndex) lookup(s string) (int, int, error) {
offsets := idx.index.Lookup([]byte(" "+s), -1)
//log.Printf("lookup(%s): %v (index bytes: '%s')\n", s, offsets, idx.index.Bytes())
if offsets == nil || len(offsets) == 0 || len(offsets) > 1 {
return -1, -1, fmt.Errorf("No such id: %s", s)
}
offsetBefore := offsets[0] + 1
offsetAfter := offsetBefore + strings.Index(string(idx.bytes[offsetBefore:]), " ")
return offsetBefore, offsetAfter, nil
}
func (idx *TruncIndex) Get(s string) (string, error) {
before, after, err := idx.lookup(s)
//log.Printf("Get(%s) bytes=|%s| before=|%d| after=|%d|\n", s, idx.bytes, before, after)
if err != nil {
return "", err
}
return string(idx.bytes[before:after]), err
}
// TruncateId returns a shorthand version of a string identifier for convenience.
// A collision with other shorthands is very unlikely, but possible.
// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller
// will need to use a langer prefix, or the full-length Id.
func TruncateId(id string) string {
shortLen := 12
if len(id) < shortLen {
shortLen = len(id)
}
return id[:shortLen]
}

View File

@ -124,3 +124,85 @@ func TestWriteBroadcaster(t *testing.T) {
writer.Close() writer.Close()
} }
// 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)
}
}