Merge branch 'master' into login_signal

* master: (54 commits)
  Return JSONError for HTTPResponse error
  Fix TestEnv
  Revert "Bind daemon to 0.0.0.0 in Vagrant. Fixes #1304"
  Add unit tests for build no cache
  Add no cache for docker build
  Move note about officially supported kernel
  Solved the logo being squished in Safari
  Add hostname to the container environment.
  fix same issue in api.go
  improve tests
  Return registy status code in error
  update http://get.docker.io/latest
  Add check that the request is good
  fix tests about refactor checksums
  add ufw doc
  Fixed a couple of minor syntax errors.
  Updated the description of run -d
  Make sure the index also receives the checksums
  Switch json/payload order
  Remove unused parameter
  ...
This commit is contained in:
David Calavera 2013-08-03 15:43:43 -07:00
commit 58e0c68132
50 changed files with 2106 additions and 1158 deletions

View File

@ -23,3 +23,5 @@ Thatcher Peskens <thatcher@dotcloud.com>
Walter Stanish <walter@pratyeka.org> Walter Stanish <walter@pratyeka.org>
<daniel@gasienica.ch> <dgasienica@zynga.com> <daniel@gasienica.ch> <dgasienica@zynga.com>
Roberto Hashioka <roberto_hashioka@hotmail.com> Roberto Hashioka <roberto_hashioka@hotmail.com>
Konstantin Pelykh <kpelykh@zettaset.com>
David Sissitka <me@dsissitka.com>

25
AUTHORS
View File

@ -4,12 +4,15 @@
# For a list of active project maintainers, see the MAINTAINERS file. # For a list of active project maintainers, see the MAINTAINERS file.
# #
Al Tobey <al@ooyala.com> Al Tobey <al@ooyala.com>
Alex Gaynor <alex.gaynor@gmail.com>
Alexey Shamrin <shamrin@gmail.com> Alexey Shamrin <shamrin@gmail.com>
Andrea Luzzardi <aluzzardi@gmail.com> Andrea Luzzardi <aluzzardi@gmail.com>
Andreas Tiefenthaler <at@an-ti.eu> Andreas Tiefenthaler <at@an-ti.eu>
Andrew Munsell <andrew@wizardapps.net> Andrew Munsell <andrew@wizardapps.net>
Andrews Medina <andrewsmedina@gmail.com>
Andy Rothfusz <github@metaliveblog.com> Andy Rothfusz <github@metaliveblog.com>
Andy Smith <github@anarkystic.com> Andy Smith <github@anarkystic.com>
Anthony Bishopric <git@anthonybishopric.com>
Antony Messerli <amesserl@rackspace.com> Antony Messerli <amesserl@rackspace.com>
Barry Allard <barry.allard@gmail.com> Barry Allard <barry.allard@gmail.com>
Brandon Liu <bdon@bdon.org> Brandon Liu <bdon@bdon.org>
@ -23,17 +26,23 @@ Daniel Gasienica <daniel@gasienica.ch>
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
Daniel Robinson <gottagetmac@gmail.com> Daniel Robinson <gottagetmac@gmail.com>
Daniel Von Fange <daniel@leancoder.com> Daniel Von Fange <daniel@leancoder.com>
Daniel YC Lin <dlin.tw@gmail.com>
David Calavera <david.calavera@gmail.com>
David Sissitka <me@dsissitka.com>
Dominik Honnef <dominik@honnef.co> Dominik Honnef <dominik@honnef.co>
Don Spaulding <donspauldingii@gmail.com> Don Spaulding <donspauldingii@gmail.com>
Dr Nic Williams <drnicwilliams@gmail.com> Dr Nic Williams <drnicwilliams@gmail.com>
Elias Probst <mail@eliasprobst.eu> Elias Probst <mail@eliasprobst.eu>
Eric Hanchrow <ehanchrow@ine.com> Eric Hanchrow <ehanchrow@ine.com>
Evan Wies <evan@neomantra.net>
Eric Myhre <hash@exultant.us> Eric Myhre <hash@exultant.us>
Erno Hopearuoho <erno.hopearuoho@gmail.com>
Evan Wies <evan@neomantra.net>
ezbercih <cem.ezberci@gmail.com> ezbercih <cem.ezberci@gmail.com>
Fabrizio Regini <freegenie@gmail.com>
Flavio Castelli <fcastelli@suse.com> Flavio Castelli <fcastelli@suse.com>
Francisco Souza <f@souza.cc> Francisco Souza <f@souza.cc>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu> Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
Gabriel Monroy <gabriel@opdemand.com>
Gareth Rushgrove <gareth@morethanseven.net> Gareth Rushgrove <gareth@morethanseven.net>
Guillaume J. Charmes <guillaume.charmes@dotcloud.com> Guillaume J. Charmes <guillaume.charmes@dotcloud.com>
Harley Laue <losinggeneration@gmail.com> Harley Laue <losinggeneration@gmail.com>
@ -41,6 +50,7 @@ Hunter Blanks <hunter@twilio.com>
Jeff Lindsay <progrium@gmail.com> Jeff Lindsay <progrium@gmail.com>
Jeremy Grosser <jeremy@synack.me> Jeremy Grosser <jeremy@synack.me>
Joffrey F <joffrey@dotcloud.com> Joffrey F <joffrey@dotcloud.com>
Johan Euphrosine <proppy@google.com>
John Costa <john.costa@gmail.com> John Costa <john.costa@gmail.com>
Jon Wedaman <jweede@gmail.com> Jon Wedaman <jweede@gmail.com>
Jonas Pfenniger <jonas@pfenniger.name> Jonas Pfenniger <jonas@pfenniger.name>
@ -48,28 +58,39 @@ Jonathan Rudenberg <jonathan@titanous.com>
Joseph Anthony Pasquale Holsten <joseph@josephholsten.com> Joseph Anthony Pasquale Holsten <joseph@josephholsten.com>
Julien Barbier <write0@gmail.com> Julien Barbier <write0@gmail.com>
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com> Jérôme Petazzoni <jerome.petazzoni@dotcloud.com>
Karan Lyons <karan@karanlyons.com>
Keli Hu <dev@keli.hu>
Ken Cochrane <kencochrane@gmail.com> Ken Cochrane <kencochrane@gmail.com>
Kevin J. Lynagh <kevin@keminglabs.com> Kevin J. Lynagh <kevin@keminglabs.com>
kim0 <email.ahmedkamal@googlemail.com> kim0 <email.ahmedkamal@googlemail.com>
Kimbro Staken <kstaken@kstaken.com>
Kiran Gangadharan <kiran.daredevil@gmail.com> Kiran Gangadharan <kiran.daredevil@gmail.com>
Konstantin Pelykh <kpelykh@zettaset.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Marco Hennings <marco.hennings@freiheit.com>
Marcus Farkas <toothlessgear@finitebox.com> Marcus Farkas <toothlessgear@finitebox.com>
Mark McGranaghan <mmcgrana@gmail.com> Mark McGranaghan <mmcgrana@gmail.com>
Maxim Treskin <zerthurd@gmail.com> Maxim Treskin <zerthurd@gmail.com>
meejah <meejah@meejah.ca> meejah <meejah@meejah.ca>
Michael Crosby <crosby.michael@gmail.com> Michael Crosby <crosby.michael@gmail.com>
Mike Gaffney <mike@uberu.com>
Mikhail Sobolev <mss@mawhrin.net> Mikhail Sobolev <mss@mawhrin.net>
Nan Monnand Deng <monnand@gmail.com>
Nate Jones <nate@endot.org> Nate Jones <nate@endot.org>
Nelson Chen <crazysim@gmail.com> Nelson Chen <crazysim@gmail.com>
Niall O'Higgins <niallo@unworkable.org> Niall O'Higgins <niallo@unworkable.org>
Nick Stenning <nick.stenning@digital.cabinet-office.gov.uk>
Nick Stinemates <nick@stinemates.org>
odk- <github@odkurzacz.org> odk- <github@odkurzacz.org>
Paul Bowsher <pbowsher@globalpersonals.co.uk> Paul Bowsher <pbowsher@globalpersonals.co.uk>
Paul Hammond <paul@paulhammond.org> Paul Hammond <paul@paulhammond.org>
Phil Spitler <pspitler@gmail.com> Phil Spitler <pspitler@gmail.com>
Piotr Bogdan <ppbogdan@gmail.com> Piotr Bogdan <ppbogdan@gmail.com>
Renato Riccieri Santos Zannon <renato.riccieri@gmail.com> Renato Riccieri Santos Zannon <renato.riccieri@gmail.com>
Rhys Hiltner <rhys@twitch.tv>
Robert Obryk <robryk@gmail.com> Robert Obryk <robryk@gmail.com>
Roberto Hashioka <roberto_hashioka@hotmail.com> Roberto Hashioka <roberto_hashioka@hotmail.com>
Ryan Fowler <rwfowler@gmail.com>
Sam Alba <sam.alba@gmail.com> Sam Alba <sam.alba@gmail.com>
Sam J Sharpe <sam.sharpe@digital.cabinet-office.gov.uk> Sam J Sharpe <sam.sharpe@digital.cabinet-office.gov.uk>
Shawn Siefkas <shawn.siefkas@meredith.com> Shawn Siefkas <shawn.siefkas@meredith.com>
@ -83,6 +104,8 @@ Thomas Hansen <thomas.hansen@gmail.com>
Tianon Gravi <admwiggin@gmail.com> Tianon Gravi <admwiggin@gmail.com>
Tim Terhorst <mynamewastaken+git@gmail.com> Tim Terhorst <mynamewastaken+git@gmail.com>
Tobias Bieniek <Tobias.Bieniek@gmx.de> Tobias Bieniek <Tobias.Bieniek@gmx.de>
Tobias Schwab <tobias.schwab@dynport.de>
Tom Hulihan <hulihan.tom159@gmail.com>
unclejack <unclejacksons@gmail.com> unclejack <unclejacksons@gmail.com>
Victor Vieux <victor.vieux@dotcloud.com> Victor Vieux <victor.vieux@dotcloud.com>
Vivek Agarwal <me@vivek.im> Vivek Agarwal <me@vivek.im>

View File

@ -50,6 +50,7 @@ release: $(BINRELEASE)
s3cmd -P put $(BINRELEASE) s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-$(RELEASE_VERSION).tgz s3cmd -P put $(BINRELEASE) s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-$(RELEASE_VERSION).tgz
s3cmd -P put docker-latest.tgz s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-latest.tgz s3cmd -P put docker-latest.tgz s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-latest.tgz
s3cmd -P put $(SRCRELEASE)/bin/docker s3://get.docker.io/builds/`uname -s`/`uname -m`/docker s3cmd -P put $(SRCRELEASE)/bin/docker s3://get.docker.io/builds/`uname -s`/`uname -m`/docker
echo $(RELEASE_VERSION) > latest ; s3cmd -P put latest s3://get.docker.io/latest ; rm latest
srcrelease: $(SRCRELEASE) srcrelease: $(SRCRELEASE)
deps: $(DOCKER_DIR) deps: $(DOCKER_DIR)
@ -65,7 +66,6 @@ $(BINRELEASE): $(SRCRELEASE)
rm -f $(BINRELEASE) rm -f $(BINRELEASE)
cd $(SRCRELEASE); make; cp -R bin docker-$(RELEASE_VERSION); tar -f ../$(BINRELEASE) -zv -c docker-$(RELEASE_VERSION) cd $(SRCRELEASE); make; cp -R bin docker-$(RELEASE_VERSION); tar -f ../$(BINRELEASE) -zv -c docker-$(RELEASE_VERSION)
cd $(SRCRELEASE); cp -R bin docker-latest; tar -f ../docker-latest.tgz -zv -c docker-latest cd $(SRCRELEASE); cp -R bin docker-latest; tar -f ../docker-latest.tgz -zv -c docker-latest
clean: clean:
@rm -rf $(dir $(DOCKER_BIN)) @rm -rf $(dir $(DOCKER_BIN))
ifeq ($(GOPATH), $(BUILD_DIR)) ifeq ($(GOPATH), $(BUILD_DIR))

View File

@ -167,7 +167,7 @@ Installing from source
---------------------- ----------------------
1. Make sure you have a [Go language](http://golang.org/doc/install) 1. Make sure you have a [Go language](http://golang.org/doc/install)
compiler and [git](http://git-scm.com) installed. compiler >= 1.1 and [git](http://git-scm.com) installed.
2. Checkout the source code 2. Checkout the source code
```bash ```bash
@ -444,51 +444,6 @@ With Standard Containers we can put an end to that embarrassment, by
making INDUSTRIAL-GRADE DELIVERY of software a reality. making INDUSTRIAL-GRADE DELIVERY of software a reality.
Standard Container Specification
--------------------------------
(TODO)
### Image format
### Standard operations
* Copy
* Run
* Stop
* Wait
* Commit
* Attach standard streams
* List filesystem changes
* ...
### Execution environment
#### Root filesystem
#### Environment variables
#### Process arguments
#### Networking
#### Process namespacing
#### Resource limits
#### Process monitoring
#### Logging
#### Signals
#### Pseudo-terminal allocation
#### Security
### Legal ### Legal
Transfers of Docker shall be in accordance with applicable export Transfers of Docker shall be in accordance with applicable export

25
api.go
View File

@ -17,7 +17,7 @@ import (
"strings" "strings"
) )
const APIVERSION = 1.3 const APIVERSION = 1.4
const DEFAULTHTTPHOST string = "127.0.0.1" const DEFAULTHTTPHOST string = "127.0.0.1"
const DEFAULTHTTPPORT int = 4243 const DEFAULTHTTPPORT int = 4243
@ -271,11 +271,18 @@ func getContainersChanges(srv *Server, version float64, w http.ResponseWriter, r
} }
func getContainersTop(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { func getContainersTop(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if version < 1.4 {
return fmt.Errorf("top was improved a lot since 1.3, Please upgrade your docker client.")
}
if vars == nil { if vars == nil {
return fmt.Errorf("Missing parameter") return fmt.Errorf("Missing parameter")
} }
if err := parseForm(r); err != nil {
return err
}
name := vars["name"] name := vars["name"]
procsStr, err := srv.ContainerTop(name) ps_args := r.Form.Get("ps_args")
procsStr, err := srv.ContainerTop(name, ps_args)
if err != nil { if err != nil {
return err return err
} }
@ -786,12 +793,8 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
remoteURL := r.FormValue("remote") remoteURL := r.FormValue("remote")
repoName := r.FormValue("t") repoName := r.FormValue("t")
rawSuppressOutput := r.FormValue("q") rawSuppressOutput := r.FormValue("q")
tag := "" rawNoCache := r.FormValue("nocache")
if strings.Contains(repoName, ":") { repoName, tag := utils.ParseRepositoryTag(repoName)
remoteParts := strings.Split(repoName, ":")
tag = remoteParts[1]
repoName = remoteParts[0]
}
var context io.Reader var context io.Reader
@ -837,8 +840,12 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
if err != nil { if err != nil {
return err return err
} }
noCache, err := getBoolParam(rawNoCache)
if err != nil {
return err
}
b := NewBuildFile(srv, utils.NewWriteFlusher(w), !suppressOutput) b := NewBuildFile(srv, utils.NewWriteFlusher(w), !suppressOutput, !noCache)
id, err := b.Build(context) id, err := b.Build(context)
if err != nil { if err != nil {
fmt.Fprintf(w, "Error build: %s\n", err) fmt.Fprintf(w, "Error build: %s\n", err)

View File

@ -30,10 +30,8 @@ type APIInfo struct {
} }
type APITop struct { type APITop struct {
PID string Titles []string
Tty string Processes [][]string
Time string
Cmd string
} }
type APIRmi struct { type APIRmi struct {

View File

@ -482,24 +482,33 @@ func TestGetContainersTop(t *testing.T) {
} }
r := httptest.NewRecorder() r := httptest.NewRecorder()
if err := getContainersTop(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil { req, err := http.NewRequest("GET", "/"+container.ID+"/top?ps_args=u", bytes.NewReader([]byte{}))
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
procs := []APITop{} if err := getContainersTop(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
t.Fatal(err)
}
procs := APITop{}
if err := json.Unmarshal(r.Body.Bytes(), &procs); err != nil { if err := json.Unmarshal(r.Body.Bytes(), &procs); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(procs) != 2 { if len(procs.Titles) != 11 {
t.Fatalf("Expected 2 processes, found %d.", len(procs)) t.Fatalf("Expected 11 titles, found %d.", len(procs.Titles))
}
if procs.Titles[0] != "USER" || procs.Titles[10] != "COMMAND" {
t.Fatalf("Expected Titles[0] to be USER and Titles[10] to be COMMAND, found %s and %s.", procs.Titles[0], procs.Titles[10])
} }
if procs[0].Cmd != "sh" && procs[0].Cmd != "busybox" { if len(procs.Processes) != 2 {
t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[0].Cmd) t.Fatalf("Expected 2 processes, found %d.", len(procs.Processes))
} }
if procs.Processes[0][10] != "/bin/sh" && procs.Processes[0][10] != "sleep" {
if procs[1].Cmd != "sh" && procs[1].Cmd != "busybox" { t.Fatalf("Expected `sleep` or `/bin/sh`, found %s.", procs.Processes[0][10])
t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[1].Cmd) }
if procs.Processes[1][10] != "/bin/sh" && procs.Processes[1][10] != "sleep" {
t.Fatalf("Expected `sleep` or `/bin/sh`, found %s.", procs.Processes[1][10])
} }
} }

View File

@ -173,7 +173,7 @@ func CopyWithTar(src, dst string) error {
} }
// Create dst, copy src's content into it // Create dst, copy src's content into it
utils.Debugf("Creating dest directory: %s", dst) utils.Debugf("Creating dest directory: %s", dst)
if err := os.MkdirAll(dst, 0700); err != nil && !os.IsExist(err) { if err := os.MkdirAll(dst, 0755); err != nil && !os.IsExist(err) {
return err return err
} }
utils.Debugf("Calling TarUntar(%s, %s)", src, dst) utils.Debugf("Calling TarUntar(%s, %s)", src, dst)

View File

@ -18,7 +18,7 @@ const CONFIGFILE = ".dockercfg"
// Only used for user auth + account creation // Only used for user auth + account creation
const INDEXSERVER = "https://index.docker.io/v1/" const INDEXSERVER = "https://index.docker.io/v1/"
//const INDEXSERVER = "http://indexstaging-docker.dotcloud.com/" //const INDEXSERVER = "https://indexstaging-docker.dotcloud.com/v1/"
var ( var (
ErrConfigFileMissing = errors.New("The Auth config file is missing") ErrConfigFileMissing = errors.New("The Auth config file is missing")

View File

@ -124,6 +124,10 @@ func (builder *Builder) Create(config *Config) (*Container, error) {
func (builder *Builder) Commit(container *Container, repository, tag, comment, author string, config *Config) (*Image, error) { func (builder *Builder) Commit(container *Container, repository, tag, comment, author string, config *Config) (*Image, error) {
// FIXME: freeze the container before copying it to avoid data corruption? // FIXME: freeze the container before copying it to avoid data corruption?
// FIXME: this shouldn't be in commands. // FIXME: this shouldn't be in commands.
if err := container.EnsureMounted(); err != nil {
return nil, err
}
rwTar, err := container.ExportRw() rwTar, err := container.ExportRw()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -11,6 +11,7 @@ import (
"os" "os"
"path" "path"
"reflect" "reflect"
"regexp"
"strings" "strings"
) )
@ -30,6 +31,7 @@ type buildFile struct {
config *Config config *Config
context string context string
verbose bool verbose bool
utilizeCache bool
tmpContainers map[string]struct{} tmpContainers map[string]struct{}
tmpImages map[string]struct{} tmpImages map[string]struct{}
@ -67,6 +69,9 @@ func (b *buildFile) CmdFrom(name string) error {
} }
b.image = image.ID b.image = image.ID
b.config = &Config{} b.config = &Config{}
if b.config.Env == nil || len(b.config.Env) == 0 {
b.config.Env = append(b.config.Env, "HOME=/", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
}
return nil return nil
} }
@ -90,6 +95,7 @@ func (b *buildFile) CmdRun(args string) error {
utils.Debugf("Command to be executed: %v", b.config.Cmd) utils.Debugf("Command to be executed: %v", b.config.Cmd)
if b.utilizeCache {
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil { if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
return err return err
} else if cache != nil { } else if cache != nil {
@ -100,6 +106,7 @@ func (b *buildFile) CmdRun(args string) error {
} else { } else {
utils.Debugf("[BUILDER] Cache miss") utils.Debugf("[BUILDER] Cache miss")
} }
}
cid, err := b.run() cid, err := b.run()
if err != nil { if err != nil {
@ -112,6 +119,40 @@ func (b *buildFile) CmdRun(args string) error {
return nil return nil
} }
func (b *buildFile) FindEnvKey(key string) int {
for k, envVar := range b.config.Env {
envParts := strings.SplitN(envVar, "=", 2)
if key == envParts[0] {
return k
}
}
return -1
}
func (b *buildFile) ReplaceEnvMatches(value string) (string, error) {
exp, err := regexp.Compile("(\\\\\\\\+|[^\\\\]|\\b|\\A)\\$({?)([[:alnum:]_]+)(}?)")
if err != nil {
return value, err
}
matches := exp.FindAllString(value, -1)
for _, match := range matches {
match = match[strings.Index(match, "$"):]
matchKey := strings.Trim(match, "${}")
for _, envVar := range b.config.Env {
envParts := strings.SplitN(envVar, "=", 2)
envKey := envParts[0]
envValue := envParts[1]
if envKey == matchKey {
value = strings.Replace(value, match, envValue, -1)
break
}
}
}
return value, nil
}
func (b *buildFile) CmdEnv(args string) error { func (b *buildFile) CmdEnv(args string) error {
tmp := strings.SplitN(args, " ", 2) tmp := strings.SplitN(args, " ", 2)
if len(tmp) != 2 { if len(tmp) != 2 {
@ -120,14 +161,19 @@ func (b *buildFile) CmdEnv(args string) error {
key := strings.Trim(tmp[0], " \t") key := strings.Trim(tmp[0], " \t")
value := strings.Trim(tmp[1], " \t") value := strings.Trim(tmp[1], " \t")
for i, elem := range b.config.Env { envKey := b.FindEnvKey(key)
if strings.HasPrefix(elem, key+"=") { replacedValue, err := b.ReplaceEnvMatches(value)
b.config.Env[i] = key + "=" + value if err != nil {
return err
}
replacedVar := fmt.Sprintf("%s=%s", key, replacedValue)
if envKey >= 0 {
b.config.Env[envKey] = replacedVar
return nil return nil
} }
} b.config.Env = append(b.config.Env, replacedVar)
b.config.Env = append(b.config.Env, key+"="+value) return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s", replacedVar))
return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, value))
} }
func (b *buildFile) CmdCmd(args string) error { func (b *buildFile) CmdCmd(args string) error {
@ -242,7 +288,7 @@ func (b *buildFile) addContext(container *Container, orig, dest string) error {
} else if err := UntarPath(origPath, destPath); err != nil { } else if err := UntarPath(origPath, destPath); err != nil {
utils.Debugf("Couldn't untar %s to %s: %s", origPath, destPath, err) utils.Debugf("Couldn't untar %s to %s: %s", origPath, destPath, err)
// If that fails, just copy it as a regular file // If that fails, just copy it as a regular file
if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil { if err := os.MkdirAll(path.Dir(destPath), 0755); err != nil {
return err return err
} }
if err := CopyWithTar(origPath, destPath); err != nil { if err := CopyWithTar(origPath, destPath); err != nil {
@ -260,8 +306,16 @@ func (b *buildFile) CmdAdd(args string) error {
if len(tmp) != 2 { if len(tmp) != 2 {
return fmt.Errorf("Invalid ADD format") return fmt.Errorf("Invalid ADD format")
} }
orig := strings.Trim(tmp[0], " \t")
dest := strings.Trim(tmp[1], " \t") orig, err := b.ReplaceEnvMatches(strings.Trim(tmp[0], " \t"))
if err != nil {
return err
}
dest, err := b.ReplaceEnvMatches(strings.Trim(tmp[1], " \t"))
if err != nil {
return err
}
cmd := b.config.Cmd cmd := b.config.Cmd
b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)} b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
@ -346,6 +400,7 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment} b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
defer func(cmd []string) { b.config.Cmd = cmd }(cmd) defer func(cmd []string) { b.config.Cmd = cmd }(cmd)
if b.utilizeCache {
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil { if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
return err return err
} else if cache != nil { } else if cache != nil {
@ -356,6 +411,8 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
} else { } else {
utils.Debugf("[BUILDER] Cache miss") utils.Debugf("[BUILDER] Cache miss")
} }
}
container, err := b.builder.Create(b.config) container, err := b.builder.Create(b.config)
if err != nil { if err != nil {
return err return err
@ -449,7 +506,7 @@ func (b *buildFile) Build(context io.Reader) (string, error) {
return "", fmt.Errorf("An error occured during the build\n") return "", fmt.Errorf("An error occured during the build\n")
} }
func NewBuildFile(srv *Server, out io.Writer, verbose bool) BuildFile { func NewBuildFile(srv *Server, out io.Writer, verbose, utilizeCache bool) BuildFile {
return &buildFile{ return &buildFile{
builder: NewBuilder(srv.runtime), builder: NewBuilder(srv.runtime),
runtime: srv.runtime, runtime: srv.runtime,
@ -459,5 +516,6 @@ func NewBuildFile(srv *Server, out io.Writer, verbose bool) BuildFile {
tmpContainers: make(map[string]struct{}), tmpContainers: make(map[string]struct{}),
tmpImages: make(map[string]struct{}), tmpImages: make(map[string]struct{}),
verbose: verbose, verbose: verbose,
utilizeCache: utilizeCache,
} }
} }

View File

@ -129,6 +129,38 @@ CMD Hello world
nil, nil,
nil, nil,
}, },
{
`
from {IMAGE}
env FOO /foo/baz
env BAR /bar
env BAZ $BAR
env FOOPATH $PATH:$FOO
run [ "$BAR" = "$BAZ" ]
run [ "$FOOPATH" = "$PATH:/foo/baz" ]
`,
nil,
nil,
},
{
`
from {IMAGE}
env FOO /bar
env TEST testdir
env BAZ /foobar
add testfile $BAZ/
add $TEST $FOO
run [ "$(cat /foobar/testfile)" = "test1" ]
run [ "$(cat /bar/withfile)" = "test2" ]
`,
[][2]string{
{"testfile", "test1"},
{"testdir/withfile", "test2"},
},
nil,
},
} }
// FIXME: test building with 2 successive overlapping ADD commands // FIXME: test building with 2 successive overlapping ADD commands
@ -163,22 +195,24 @@ func mkTestingFileServer(files [][2]string) (*httptest.Server, error) {
func TestBuild(t *testing.T) { func TestBuild(t *testing.T) {
for _, ctx := range testContexts { for _, ctx := range testContexts {
buildImage(ctx, t) buildImage(ctx, t, nil, true)
} }
} }
func buildImage(context testContextTemplate, t *testing.T) *Image { func buildImage(context testContextTemplate, t *testing.T, srv *Server, useCache bool) *Image {
if srv == nil {
runtime, err := newTestRuntime() runtime, err := newTestRuntime()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer nuke(runtime) defer nuke(runtime)
srv := &Server{ srv = &Server{
runtime: runtime, runtime: runtime,
pullingPool: make(map[string]struct{}), pullingPool: make(map[string]struct{}),
pushingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}),
} }
}
httpServer, err := mkTestingFileServer(context.remoteFiles) httpServer, err := mkTestingFileServer(context.remoteFiles)
if err != nil { if err != nil {
@ -192,10 +226,10 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
} }
port := httpServer.URL[idx+1:] port := httpServer.URL[idx+1:]
ip := runtime.networkManager.bridgeNetwork.IP ip := srv.runtime.networkManager.bridgeNetwork.IP
dockerfile := constructDockerfile(context.dockerfile, ip, port) dockerfile := constructDockerfile(context.dockerfile, ip, port)
buildfile := NewBuildFile(srv, ioutil.Discard, false) buildfile := NewBuildFile(srv, ioutil.Discard, false, useCache)
id, err := buildfile.Build(mkTestContext(dockerfile, context.files, t)) id, err := buildfile.Build(mkTestContext(dockerfile, context.files, t))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -213,7 +247,7 @@ func TestVolume(t *testing.T) {
from {IMAGE} from {IMAGE}
volume /test volume /test
cmd Hello world cmd Hello world
`, nil, nil}, t) `, nil, nil}, t, nil, true)
if len(img.Config.Volumes) == 0 { if len(img.Config.Volumes) == 0 {
t.Fail() t.Fail()
@ -229,7 +263,7 @@ func TestBuildMaintainer(t *testing.T) {
img := buildImage(testContextTemplate{` img := buildImage(testContextTemplate{`
from {IMAGE} from {IMAGE}
maintainer dockerio maintainer dockerio
`, nil, nil}, t) `, nil, nil}, t, nil, true)
if img.Author != "dockerio" { if img.Author != "dockerio" {
t.Fail() t.Fail()
@ -241,9 +275,15 @@ func TestBuildEnv(t *testing.T) {
from {IMAGE} from {IMAGE}
env port 4243 env port 4243
`, `,
nil, nil}, t) nil, nil}, t, nil, true)
hasEnv := false
if img.Config.Env[0] != "port=4243" { for _, envVar := range img.Config.Env {
if envVar == "port=4243" {
hasEnv = true
break
}
}
if !hasEnv {
t.Fail() t.Fail()
} }
} }
@ -253,7 +293,7 @@ func TestBuildCmd(t *testing.T) {
from {IMAGE} from {IMAGE}
cmd ["/bin/echo", "Hello World"] cmd ["/bin/echo", "Hello World"]
`, `,
nil, nil}, t) nil, nil}, t, nil, true)
if img.Config.Cmd[0] != "/bin/echo" { if img.Config.Cmd[0] != "/bin/echo" {
t.Log(img.Config.Cmd[0]) t.Log(img.Config.Cmd[0])
@ -270,7 +310,7 @@ func TestBuildExpose(t *testing.T) {
from {IMAGE} from {IMAGE}
expose 4243 expose 4243
`, `,
nil, nil}, t) nil, nil}, t, nil, true)
if img.Config.PortSpecs[0] != "4243" { if img.Config.PortSpecs[0] != "4243" {
t.Fail() t.Fail()
@ -282,8 +322,70 @@ func TestBuildEntrypoint(t *testing.T) {
from {IMAGE} from {IMAGE}
entrypoint ["/bin/echo"] entrypoint ["/bin/echo"]
`, `,
nil, nil}, t) nil, nil}, t, nil, true)
if img.Config.Entrypoint[0] != "/bin/echo" { if img.Config.Entrypoint[0] != "/bin/echo" {
} }
} }
func TestBuildImageWithCache(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
srv := &Server{
runtime: runtime,
pullingPool: make(map[string]struct{}),
pushingPool: make(map[string]struct{}),
}
template := testContextTemplate{`
from {IMAGE}
maintainer dockerio
`,
nil, nil}
img := buildImage(template, t, srv, true)
imageId := img.ID
img = nil
img = buildImage(template, t, srv, true)
if imageId != img.ID {
t.Logf("Image ids should match: %s != %s", imageId, img.ID)
t.Fail()
}
}
func TestBuildImageWithoutCache(t *testing.T) {
runtime, err := newTestRuntime()
if err != nil {
t.Fatal(err)
}
defer nuke(runtime)
srv := &Server{
runtime: runtime,
pullingPool: make(map[string]struct{}),
pushingPool: make(map[string]struct{}),
}
template := testContextTemplate{`
from {IMAGE}
maintainer dockerio
`,
nil, nil}
img := buildImage(template, t, srv, true)
imageId := img.ID
img = nil
img = buildImage(template, t, srv, false)
if imageId == img.ID {
t.Logf("Image ids should not match: %s == %s", imageId, img.ID)
t.Fail()
}
}

View File

@ -31,6 +31,7 @@ const VERSION = "0.5.0-dev"
var ( var (
GITCOMMIT string GITCOMMIT string
AuthRequiredError = fmt.Errorf("Authentication is required.")
) )
func (cli *DockerCli) getMethod(name string) (reflect.Method, bool) { func (cli *DockerCli) getMethod(name string) (reflect.Method, bool) {
@ -160,6 +161,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
cmd := Subcmd("build", "[OPTIONS] PATH | URL | -", "Build a new container image from the source code at PATH") cmd := Subcmd("build", "[OPTIONS] PATH | URL | -", "Build a new container image from the source code at PATH")
tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success") tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success")
suppressOutput := cmd.Bool("q", false, "Suppress verbose build output") suppressOutput := cmd.Bool("q", false, "Suppress verbose build output")
noCache := cmd.Bool("no-cache", false, "Do not use cache when building the image")
if err := cmd.Parse(args); err != nil { if err := cmd.Parse(args); err != nil {
return nil return nil
@ -208,6 +210,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
if isRemote { if isRemote {
v.Set("remote", cmd.Arg(0)) v.Set("remote", cmd.Arg(0))
} }
if *noCache {
v.Set("nocache", "1")
}
req, err := http.NewRequest("POST", fmt.Sprintf("/v%g/build?%s", APIVERSION, v.Encode()), body) req, err := http.NewRequest("POST", fmt.Sprintf("/v%g/build?%s", APIVERSION, v.Encode()), body)
if err != nil { if err != nil {
return err return err
@ -314,13 +319,21 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
email string email string
) )
var promptDefault = func(prompt string, configDefault string) {
if configDefault == "" {
fmt.Fprintf(cli.out, "%s: ", prompt)
} else {
fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault)
}
}
authconfig, ok := cli.configFile.Configs[auth.IndexServerAddress()] authconfig, ok := cli.configFile.Configs[auth.IndexServerAddress()]
if !ok { if !ok {
authconfig = auth.AuthConfig{} authconfig = auth.AuthConfig{}
} }
if *flUsername == "" { if *flUsername == "" {
fmt.Fprintf(cli.out, "Username (%s): ", authconfig.Username) promptDefault("Username", authconfig.Username)
username = readAndEchoString(cli.in, cli.out) username = readAndEchoString(cli.in, cli.out)
if username == "" { if username == "" {
username = authconfig.Username username = authconfig.Username
@ -340,7 +353,7 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
} }
if *flEmail == "" { if *flEmail == "" {
fmt.Fprintf(cli.out, "Email (%s): ", authconfig.Email) promptDefault("Email", authconfig.Email)
email = readAndEchoString(cli.in, cli.out) email = readAndEchoString(cli.in, cli.out)
if email == "" { if email == "" {
email = authconfig.Email email = authconfig.Email
@ -440,6 +453,15 @@ func (cli *DockerCli) CmdVersion(args ...string) error {
if out.GoVersion != "" { if out.GoVersion != "" {
fmt.Fprintf(cli.out, "Go version: %s\n", out.GoVersion) fmt.Fprintf(cli.out, "Go version: %s\n", out.GoVersion)
} }
release := utils.GetReleaseVersion()
if release != "" {
fmt.Fprintf(cli.out, "Last stable version: %s", release)
if strings.Trim(VERSION, "-dev") != release || strings.Trim(out.Version, "-dev") != release {
fmt.Fprintf(cli.out, ", please update docker")
}
fmt.Fprintf(cli.out, "\n")
}
return nil return nil
} }
@ -596,23 +618,28 @@ func (cli *DockerCli) CmdTop(args ...string) error {
if err := cmd.Parse(args); err != nil { if err := cmd.Parse(args); err != nil {
return nil return nil
} }
if cmd.NArg() != 1 { if cmd.NArg() == 0 {
cmd.Usage() cmd.Usage()
return nil return nil
} }
body, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top", nil) val := url.Values{}
if cmd.NArg() > 1 {
val.Set("ps_args", strings.Join(cmd.Args()[1:], " "))
}
body, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top?"+val.Encode(), nil)
if err != nil { if err != nil {
return err return err
} }
var procs []APITop procs := APITop{}
err = json.Unmarshal(body, &procs) err = json.Unmarshal(body, &procs)
if err != nil { if err != nil {
return err return err
} }
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
fmt.Fprintln(w, "PID\tTTY\tTIME\tCMD") fmt.Fprintln(w, strings.Join(procs.Titles, "\t"))
for _, proc := range procs { for _, proc := range procs.Processes {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", proc.PID, proc.Tty, proc.Time, proc.Cmd) fmt.Fprintln(w, strings.Join(proc, "\t"))
} }
w.Flush() w.Flush()
return nil return nil
@ -801,10 +828,6 @@ func (cli *DockerCli) CmdPush(args ...string) error {
return nil return nil
} }
if err := cli.checkIfLogged("push"); err != nil {
return err
}
// If we're not using a custom registry, we know the restrictions // If we're not using a custom registry, we know the restrictions
// applied to repository names and can warn the user in advance. // applied to repository names and can warn the user in advance.
// Custom repositories can have different rules, and we must also // Custom repositories can have different rules, and we must also
@ -813,13 +836,22 @@ func (cli *DockerCli) CmdPush(args ...string) error {
return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.configFile.Configs[auth.IndexServerAddress()].Username, name) return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.configFile.Configs[auth.IndexServerAddress()].Username, name)
} }
v := url.Values{}
push := func() error {
buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()]) buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
if err != nil { if err != nil {
return err return err
} }
v := url.Values{} return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), cli.out)
if err := cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), cli.out); err != nil { }
if err := push(); err != nil {
if err == AuthRequiredError {
if err = cli.checkIfLogged("push"); err == nil {
return push()
}
}
return err return err
} }
return nil return nil
@ -1546,6 +1578,9 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
} else if err != nil { } else if err != nil {
return err return err
} }
if jm.Error != nil && jm.Error.Code == 401 {
return AuthRequiredError
}
jm.Display(out) jm.Display(out)
} }
} else { } else {
@ -1658,14 +1693,13 @@ func (cli *DockerCli) monitorTtySize(id string) error {
} }
cli.resizeTty(id) cli.resizeTty(id)
c := make(chan os.Signal, 1) sigchan := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGWINCH) signal.Notify(sigchan, syscall.SIGWINCH)
go func() { go func() {
for sig := range c { for {
if sig == syscall.SIGWINCH { <-sigchan
cli.resizeTty(id) cli.resizeTty(id)
} }
}
}() }()
return nil return nil
} }

View File

@ -52,7 +52,7 @@ type Container struct {
waitLock chan struct{} waitLock chan struct{}
Volumes map[string]string Volumes map[string]string
// Store rw/ro in a separate structure to preserve reserve-compatibility on-disk. // Store rw/ro in a separate structure to preserve reverse-compatibility on-disk.
// Easier than migrating older container configs :) // Easier than migrating older container configs :)
VolumesRW map[string]bool VolumesRW map[string]bool
} }
@ -100,7 +100,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
flHostname := cmd.String("h", "", "Container host name") flHostname := cmd.String("h", "", "Container host name")
flUser := cmd.String("u", "", "Username or UID") flUser := cmd.String("u", "", "Username or UID")
flDetach := cmd.Bool("d", false, "Detached mode: leave the container running in the background") flDetach := cmd.Bool("d", false, "Detached mode: Run container in the background, print new container id")
flAttach := NewAttachOpts() flAttach := NewAttachOpts()
cmd.Var(flAttach, "a", "Attach to stdin, stdout or stderr.") cmd.Var(flAttach, "a", "Attach to stdin, stdout or stderr.")
flStdin := cmd.Bool("i", false, "Keep stdin open even if not attached") flStdin := cmd.Bool("i", false, "Keep stdin open even if not attached")
@ -266,7 +266,8 @@ func (container *Container) FromDisk() error {
return err return err
} }
// Load container settings // Load container settings
if err := json.Unmarshal(data, container); err != nil { // udp broke compat of docker.PortMapping, but it's not used when loading a container, we can skip it
if err := json.Unmarshal(data, container); err != nil && !strings.Contains(err.Error(), "docker.PortMapping") {
return err return err
} }
return nil return nil
@ -652,6 +653,7 @@ func (container *Container) Start(hostConfig *HostConfig) error {
"-e", "HOME=/", "-e", "HOME=/",
"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"-e", "container=lxc", "-e", "container=lxc",
"-e", "HOSTNAME="+container.Config.Hostname,
) )
for _, elem := range container.Config.Env { for _, elem := range container.Config.Env {

View File

@ -960,6 +960,7 @@ func TestEnv(t *testing.T) {
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/", "HOME=/",
"container=lxc", "container=lxc",
"HOSTNAME=" + container.ShortID(),
} }
sort.Strings(goodEnv) sort.Strings(goodEnv)
if len(goodEnv) != len(actualEnv) { if len(goodEnv) != len(actualEnv) {

View File

@ -26,15 +26,15 @@ Docker Remote API
2. Versions 2. Versions
=========== ===========
The current verson of the API is 1.3 The current verson of the API is 1.4
Calling /images/<name>/insert is the same as calling Calling /images/<name>/insert is the same as calling
/v1.3/images/<name>/insert /v1.4/images/<name>/insert
You can still call an old version of the api using You can still call an old version of the api using
/v1.0/images/<name>/insert /v1.0/images/<name>/insert
:doc:`docker_remote_api_v1.3` :doc:`docker_remote_api_v1.4`
***************************** *****************************
What's new What's new
@ -42,7 +42,19 @@ What's new
.. http:get:: /containers/(id)/top .. http:get:: /containers/(id)/top
**New!** List the processes running inside a container. **New!** You can now use ps args with docker top, like `docker top <container_id> aux`
:doc:`docker_remote_api_v1.3`
*****************************
docker v0.5.0 51f6c4a_
What's new
----------
.. http:get:: /containers/(id)/top
List the processes running inside a container.
.. http:get:: /events: .. http:get:: /events:
@ -138,6 +150,7 @@ Initial version
.. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f .. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f
.. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4 .. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4
.. _2e7649b: https://github.com/dotcloud/docker/commit/2e7649beda7c820793bd46766cbc2cfeace7b168 .. _2e7649b: https://github.com/dotcloud/docker/commit/2e7649beda7c820793bd46766cbc2cfeace7b168
.. _51f6c4a: https://github.com/dotcloud/docker/commit/51f6c4a7372450d164c61e0054daf0223ddbd909
================================== ==================================
Docker Remote API Client Libraries Docker Remote API Client Libraries

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
Build a new container image from the source code at PATH Build a new container image from the source code at PATH
-t="": Tag to be applied to the resulting image in case of success. -t="": Tag to be applied to the resulting image in case of success.
-q=false: Suppress verbose build output. -q=false: Suppress verbose build output.
-no-cache: Do not use the cache when building the image.
When a single Dockerfile is given as URL, then no context is set. When a git repository is set as URL, the repository is used as context When a single Dockerfile is given as URL, then no context is set. When a git repository is set as URL, the repository is used as context

View File

@ -15,7 +15,7 @@
-a=map[]: Attach to stdin, stdout or stderr. -a=map[]: Attach to stdin, stdout or stderr.
-c=0: CPU shares (relative weight) -c=0: CPU shares (relative weight)
-cidfile="": Write the container ID to the file -cidfile="": Write the container ID to the file
-d=false: Detached mode: leave the container running in the background -d=false: Detached mode: Run container in the background, print new container id
-e=[]: Set environment variables -e=[]: Set environment variables
-h="": Container host name -h="": Container host name
-i=false: Keep stdin open even if not attached -i=false: Keep stdin open even if not attached

View File

@ -4,10 +4,6 @@
.. _dockermanifesto: .. _dockermanifesto:
*(This was our original Welcome page, but it is a bit forward-looking
for docs, and maybe not enough vision for a true manifesto. We'll
reveal more vision in the future to make it more Manifesto-y.)*
Docker Manifesto Docker Manifesto
---------------- ----------------
@ -131,60 +127,3 @@ sitting 10 miles away.
With Standard Containers we can put an end to that embarrassment, by With Standard Containers we can put an end to that embarrassment, by
making INDUSTRIAL-GRADE DELIVERY of software a reality. making INDUSTRIAL-GRADE DELIVERY of software a reality.
Standard Container Specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(TODO)
Image format
~~~~~~~~~~~~
Standard operations
~~~~~~~~~~~~~~~~~~~
- Copy
- Run
- Stop
- Wait
- Commit
- Attach standard streams
- List filesystem changes
- ...
Execution environment
~~~~~~~~~~~~~~~~~~~~~
Root filesystem
^^^^^^^^^^^^^^^
Environment variables
^^^^^^^^^^^^^^^^^^^^^
Process arguments
^^^^^^^^^^^^^^^^^
Networking
^^^^^^^^^^
Process namespacing
^^^^^^^^^^^^^^^^^^^
Resource limits
^^^^^^^^^^^^^^^
Process monitoring
^^^^^^^^^^^^^^^^^^
Logging
^^^^^^^
Signals
^^^^^^^
Pseudo-terminal allocation
^^^^^^^^^^^^^^^^^^^^^^^^^^
Security
^^^^^^^^

View File

@ -39,7 +39,7 @@ This time, we're requesting shared access to $COUCH1's volumes.
.. code-block:: bash .. code-block:: bash
COUCH2=$(docker run -d -volumes-from $COUCH1) shykes/couchdb:2013-05-03) COUCH2=$(docker run -d -volumes-from $COUCH1 shykes/couchdb:2013-05-03)
Browse data on the second database Browse data on the second database
---------------------------------- ----------------------------------
@ -48,6 +48,6 @@ Browse data on the second database
HOST=localhost HOST=localhost
URL="http://$HOST:$(docker port $COUCH2 5984)/_utils/" URL="http://$HOST:$(docker port $COUCH2 5984)/_utils/"
echo "Navigate to $URL in your browser. You should see the same data as in the first database!" echo "Navigate to $URL in your browser. You should see the same data as in the first database"'!'
Congratulations, you are running 2 Couchdb containers, completely isolated from each other *except* for their data. Congratulations, you are running 2 Couchdb containers, completely isolated from each other *except* for their data.

View File

@ -15,12 +15,11 @@ In short, Docker has the following kernel requirements:
- Cgroups and namespaces must be enabled. - Cgroups and namespaces must be enabled.
The officially supported kernel is the one recommended by the
The officially supported kernel is the one recommended by the :ref:`ubuntu_linux` installation path. It is the one that most developers
:ref:`ubuntu_linux` installation path. It is the one that most developers will use, and the one that receives the most attention from the core
will use, and the one that receives the most attention from the core contributors. If you decide to go with a different kernel and hit a bug,
contributors. If you decide to go with a different kernel and hit a bug, please try to reproduce it with the official kernels first.
please try to reproduce it with the official kernels first.
If you cannot or do not want to use the "official" kernels, If you cannot or do not want to use the "official" kernels,
here is some technical background about the features (both optional and here is some technical background about the features (both optional and

View File

@ -19,6 +19,8 @@ Docker has the following dependencies
* Linux kernel 3.8 (read more about :ref:`kernel`) * Linux kernel 3.8 (read more about :ref:`kernel`)
* AUFS file system support (we are working on BTRFS support as an alternative) * AUFS file system support (we are working on BTRFS support as an alternative)
Please read :ref:`ufw`, if you plan to use `UFW (Uncomplicated Firewall) <https://help.ubuntu.com/community/UFW>`_
.. _ubuntu_precise: .. _ubuntu_precise:
Ubuntu Precise 12.04 (LTS) (64-bit) Ubuntu Precise 12.04 (LTS) (64-bit)
@ -135,3 +137,35 @@ Verify it worked
**Done!**, now continue with the :ref:`hello_world` example. **Done!**, now continue with the :ref:`hello_world` example.
.. _ufw:
Docker and UFW
^^^^^^^^^^^^^^
Docker uses a bridge to manage containers networking, by default UFW drop all `forwarding`, a first step is to enable forwarding:
.. code-block:: bash
sudo nano /etc/default/ufw
----
# Change:
# DEFAULT_FORWARD_POLICY="DROP"
# to
DEFAULT_FORWARD_POLICY="ACCEPT"
Then reload UFW:
.. code-block:: bash
sudo ufw reload
UFW's default set of rules denied all `incoming`, so if you want to be able to reach your containers from another host,
you should allow incoming connexions on the docker port (default 4243):
.. code-block:: bash
sudo ufw allow 4243/tcp

View File

@ -182,7 +182,7 @@ The copy obeys the following rules:
written at ``<dst>``. written at ``<dst>``.
* If ``<dest>`` doesn't exist, it is created along with all missing * If ``<dest>`` doesn't exist, it is created along with all missing
directories in its path. All new files and directories are created directories in its path. All new files and directories are created
with mode 0700, uid and gid 0. with mode 0755, uid and gid 0.
3.8 ENTRYPOINT 3.8 ENTRYPOINT
-------------- --------------

View File

@ -1 +1 @@
Thatcher Penskens <thatcher@dotcloud.com> Thatcher Peskens <thatcher@dotcloud.com>

View File

@ -68,18 +68,18 @@
<div style="float: right" class="pull-right"> <div style="float: right" class="pull-right">
<ul class="nav"> <ul class="nav">
<li id="nav-introduction"><a href="http://www.docker.io/">Home</a></li> <li id="nav-introduction"><a href="http://www.docker.io/" title="Docker Homepage">Home</a></li>
<li id="nav-about"><a href="http://www.docker.io/">About</a></li> <li id="nav-about"><a href="http://www.docker.io/about/" title="About">About</a></li>
<li id="nav-community"><a href="http://www.docker.io/">Community</a></li> <li id="nav-community"><a href="http://www.docker.io/community/" title="Community">Community</a></li>
<li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li> <li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
<li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li> <li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li> <li id="nav-blog"><a href="http://blog.docker.io/" title="Docker Blog">Blog</a></li>
<li id="nav-index"><a href="http://index.docker.io/" title="Docker Image Index, find images here">INDEX <img class="inline-icon" src="{{ pathto('_static/img/external-link-icon.png', 1) }}" title="external link"> </a></li> <li id="nav-index"><a href="http://index.docker.io/" title="Docker Image Index, find images here">INDEX <img class="inline-icon" src="{{ pathto('_static/img/external-link-icon.png', 1) }}" title="external link"> </a></li>
</ul> </ul>
</div> </div>
<div style="margin-left: -12px; float: left;"> <div style="margin-left: -12px; float: left;">
<a href="http://www.docker.io" title="Docker Homepage"><img style="margin-top: 0px; height: 60px; margin-left: 10px;" src="{{ pathto('_static/img/docker-top-logo.png', 1) }}"></a> <a href="http://www.docker.io" title="Docker Homepage"><img style="margin-top: 0px; height: 60px; width: 160px; margin-left: 10px;" src="{{ pathto('_static/img/docker-top-logo.png', 1) }}"></a>
</div> </div>
</div> </div>

View File

@ -1 +0,0 @@
Thatcher Penskens <thatcher@dotcloud.com>

View File

@ -1,2 +0,0 @@
www:
type: static

View File

@ -1,220 +0,0 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Docker - the Linux container runtime</title>
<meta name="description" content="Docker encapsulates heterogeneous payloads in standard containers">
<meta name="viewport" content="width=device-width">
<!-- twitter bootstrap -->
<link rel="stylesheet" href="../static/css/bootstrap.min.css">
<link rel="stylesheet" href="../static/css/bootstrap-responsive.min.css">
<!-- main style file -->
<link rel="stylesheet" href="../static/css/main.css">
<!-- vendor scripts -->
<script src="../static/js/vendor/jquery-1.9.1.min.js" type="text/javascript" ></script>
<script src="../static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js" type="text/javascript" ></script>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-dotcloud">
<div class="container" style="text-align: center;">
<div style="float: right" class="pull-right">
<ul class="nav">
<li id="nav-introduction"><a href="../">Introduction</a></li>
<li id="nav-gettingstarted" class="active"><a href="">Getting started</a></li>
<li id="nav-documentation" class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
</ul>
</div>
<div style="margin-left: -12px; float: left;">
<a href="../index.html"><img style="margin-top: 12px; height: 38px" src="../static/img/docker-letters-logo.gif"></a>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span12 titlebar">
<div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
<a href="http://github.com/dotcloud/docker/"><img src="../static/img/fork-us.png"> Fork us on Github</a>
</div>
<h1 class="pageheader"> GETTING STARTED</h1>
</div>
</div>
</div>
<div class="container">
<div class="alert alert-info" style="margin-bottom: 0;">
<strong>Docker is still under heavy development.</strong> It should not yet be used in production. Check <a href="http://github.com/dotcloud/docker">the repo</a> for recent progress.
</div>
<div class="row">
<div class="span6">
<section class="contentblock">
<h2>
<a name="installing-on-ubuntu-1204-and-1210" class="anchor" href="#installing-on-ubuntu-1204-and-1210"><span class="mini-icon mini-icon-link"></span>
</a>Installing on Ubuntu</h2>
<p><strong>Requirements</strong></p>
<ul>
<li>Ubuntu 12.04 (LTS) (64-bit)</li>
<li> or Ubuntu 12.10 (quantal) (64-bit)</li>
<li>The 3.8 Linux Kernel</li>
</ul>
<ol>
<li>
<p><strong>Install dependencies</strong></p>
The linux-image-extra package is only needed on standard Ubuntu EC2 AMIs in order to install the aufs kernel module.
<pre>sudo apt-get install linux-image-extra-`uname -r`</pre>
</li>
<li>
<p><strong>Install Docker</strong></p>
<p>Add the Ubuntu PPA (Personal Package Archive) sources to your apt sources list, update and install.</p>
<p>This may import a new GPG key (key 63561DC6: public key "Launchpad PPA for dotcloud team" imported).</p>
<div class="highlight">
<pre>sudo apt-get install software-properties-common</pre>
<pre>sudo add-apt-repository ppa:dotcloud/lxc-docker</pre>
<pre>sudo apt-get update</pre>
<pre>sudo apt-get install lxc-docker</pre>
</div>
</li>
<li>
<p><strong>Run!</strong></p>
<div class="highlight">
<pre>docker run -i -t ubuntu /bin/bash</pre>
</div>
</li>
Continue with the <a href="http://docs.docker.io/en/latest/examples/hello_world/">Hello world</a> example.<br>
Or check <a href="http://docs.docker.io/en/latest/installation/ubuntulinux/">more detailed installation instructions</a>
</ol>
</section>
<section class="contentblock">
<h2>Contributing to Docker</h2>
<p>Want to hack on Docker? Awesome! We have some <a href="http://docs.docker.io/en/latest/contributing/contributing/">instructions to get you started</a>. They are probably not perfect, please let us know if anything feels wrong or incomplete.</p>
</section>
</div>
<div class="span6">
<section class="contentblock">
<h2>Quick install on other operating systems</h2>
<p><strong>For other operating systems we recommend and provide a streamlined install with virtualbox,
vagrant and an Ubuntu virtual machine.</strong></p>
<ul>
<li><a href="http://docs.docker.io/en/latest/installation/vagrant/">Mac OS X and other linuxes</a></li>
<li><a href="http://docs.docker.io/en/latest/installation/windows/">Windows</a></li>
</ul>
</section>
<section class="contentblock">
<h2>Questions? Want to get in touch?</h2>
<p>There are several ways to get in touch:</p>
<p><strong>Join the discussion on IRC.</strong> We can be found in the <a href="irc://chat.freenode.net#docker">#docker</a> channel on chat.freenode.net</p>
<p><strong>Discussions</strong> happen on our google group: <a href="https://groups.google.com/d/forum/docker-club">docker-club at googlegroups.com</a></p>
<p>All our <strong>development and decisions</strong> are made out in the open on Github <a href="http://www.github.com/dotcloud/docker">github.com/dotcloud/docker</a></p>
<p><strong>Get help on using Docker</strong> by asking on <a href="http://stackoverflow.com/tags/docker/">Stackoverflow</a></p>
<p>And of course, <strong>tweet</strong> your tweets to <a href="http://twitter.com/getdocker/">twitter.com/getdocker</a></p>
</section>
<section class="contentblock">
<div id="wufoo-z7x3p3">
Fill out my <a href="http://dotclouddocker.wufoo.com/forms/z7x3p3">online form</a>.
</div>
<script type="text/javascript">var z7x3p3;(function(d, t) {
var s = d.createElement(t), options = {
'userName':'dotclouddocker',
'formHash':'z7x3p3',
'autoResize':true,
'height':'577',
'async':true,
'header':'show'};
s.src = ('https:' == d.location.protocol ? 'https://' : 'http://') + 'wufoo.com/scripts/embed/form.js';
s.onload = s.onreadystatechange = function() {
var rs = this.readyState; if (rs) if (rs != 'complete') if (rs != 'loaded') return;
try { z7x3p3 = new WufooForm();z7x3p3.initialize(options);z7x3p3.display(); } catch (e) {}};
var scr = d.getElementsByTagName(t)[0], par = scr.parentNode; par.insertBefore(s, scr);
})(document, 'script');</script>
</section>
</div>
</div>
</div>
<div class="container">
<footer id="footer" class="footer">
<div class="row">
<div class="span12 social">
<div class="tbox textright forceleftmargin social links pull-right">
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
</div>
Docker is a project by <a href="http://www.dotcloud.com">dotCloud</a>
</div>
</div>
<div class="row">
<div class="emptyspace" style="height: 40px">
</div>
</div>
</footer>
</div>
<!-- bootstrap javascipts -->
<script src="../static/js/vendor/bootstrap.min.js" type="text/javascript"></script>
<!-- Google analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-6096819-11']);
_gaq.push(['_setDomainName', 'docker.io']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>

View File

@ -1,359 +0,0 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" xmlns="http://www.w3.org/1999/html"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="google-site-verification" content="UxV66EKuPe87dgnH1sbrldrx6VsoWMrx5NjwkgUFxXI" />
<title>Docker - the Linux container engine</title>
<meta name="description" content="Docker encapsulates heterogeneous payloads in standard containers">
<meta name="viewport" content="width=device-width">
<!-- twitter bootstrap -->
<link rel="stylesheet" href="static/css/bootstrap.min.css">
<link rel="stylesheet" href="static/css/bootstrap-responsive.min.css">
<!-- main style file -->
<link rel="stylesheet" href="static/css/main.css">
<!-- vendor scripts -->
<script src="static/js/vendor/jquery-1.9.1.min.js" type="text/javascript" ></script>
<script src="static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js" type="text/javascript" ></script>
<style>
.indexlabel {
float: left;
width: 150px;
display: block;
padding: 10px 20px 10px;
font-size: 20px;
font-weight: 200;
background-color: #a30000;
color: white;
height: 22px;
}
.searchbutton {
font-size: 20px;
height: 40px;
}
.debug {
border: 1px red dotted;
}
.twitterblock {
min-height: 75px;
}
.twitterblock img {
float: left;
margin-right: 10px;
}
</style>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-dotcloud">
<div class="container" style="text-align: center;">
<div class="pull-left" id="fork-us" style="margin-top: 16px;">
<a href="http://github.com/dotcloud/docker/"><img src="static/img/fork-us.png" alt="fork-icon"> Fork us on Github</a>
</div>
<div class="pull-right" >
<ul class="nav">
<li id="nav-introduction" class="active"><a href="/">Introduction</a></li>
<li id="nav-gettingstarted"><a href="gettingstarted">Getting started</a></li>
<li id="nav-documentation" class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="container" style="margin-top: 30px;">
<div class="row">
<div class="span12">
<section class="contentblock header">
<div class="span5" style="margin-bottom: 15px;">
<div style="text-align: center;" >
<img src="static/img/docker_letters_500px.png" alt="docker letters">
<h2>The Linux container engine</h2>
</div>
<div style="display: block; text-align: center; margin-top: 20px;">
<h5>
Docker is an open-source engine which automates the deployment of applications as highly portable, self-sufficient containers which are independent of hardware, language, framework, packaging system and hosting provider.
</h5>
</div>
<div style="display: block; text-align: center; margin-top: 30px;">
<a class="btn btn-custom btn-large" href="gettingstarted/">Let's get started</a>
</div>
</div>
<div class="span6" >
<div class="js-video" >
<iframe width="600" height="360" src="http://www.youtube.com/embed/wW9CAH9nSLs?feature=player_detailpage&rel=0&modestbranding=1&start=11" frameborder="0" allowfullscreen></iframe>
</div>
</div>
<br style="clear: both"/>
</section>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span6">
<section class="contentblock">
<h4>Heterogeneous payloads</h4>
<p>Any combination of binaries, libraries, configuration files, scripts, virtualenvs, jars, gems, tarballs, you name it. No more juggling between domain-specific tools. Docker can deploy and run them all.</p>
<h4>Any server</h4>
<p>Docker can run on any x64 machine with a modern linux kernel - whether it's a laptop, a bare metal server or a VM. This makes it perfect for multi-cloud deployments.</p>
<h4>Isolation</h4>
<p>Docker isolates processes from each other and from the underlying host, using lightweight containers.</p>
<h4>Repeatability</h4>
<p>Because each container is isolated in its own filesystem, they behave the same regardless of where, when, and alongside what they run.</p>
</section>
<section class="contentblock">
<div class="container">
<div class="span2" style="margin-left: 0" >
<a href="http://dotcloud.theresumator.com/apply/mWjkD4/Software-Engineer.html" title="Job description"><img src="static/img/hiring_graphic.png" alt="we're hiring" width="140" style="margin-top: 25px"></a>
</div>
<div class="span4" style="margin-left: 0">
<h4>Do you think it is cool to hack on docker? Join us!</h4>
<ul>
<li>Work on open source</li>
<li>Program in Go</li>
</ul>
<a href="http://dotcloud.theresumator.com/apply/mWjkD4/Software-Engineer.html" title="Job description">read more</a>
</div>
</div>
</section>
</div>
<div class="span6">
<section class="contentblock">
<h1>New! Docker Index</h1>
On the Docker Index you can find and explore pre-made container images. It allows you to share your images and download them.
<br><br>
<a href="https://index.docker.io" target="_blank">
<div class="indexlabel">
DOCKER index
</div>
</a>
&nbsp;
<input type="button" class="searchbutton" value="Search images"
onClick="window.open('https://index.docker.io')" />
</section>
<section class="contentblock">
<div id="wufoo-z7x3p3">
Fill out my <a href="http://dotclouddocker.wufoo.com/forms/z7x3p3">online form</a>.
</div>
<script type="text/javascript">var z7x3p3;(function(d, t) {
var s = d.createElement(t), options = {
'userName':'dotclouddocker',
'formHash':'z7x3p3',
'autoResize':true,
'height':'577',
'async':true,
'header':'show'};
s.src = ('https:' == d.location.protocol ? 'https://' : 'http://') + 'wufoo.com/scripts/embed/form.js';
s.onload = s.onreadystatechange = function() {
var rs = this.readyState; if (rs) if (rs != 'complete') if (rs != 'loaded') return;
try { z7x3p3 = new WufooForm();z7x3p3.initialize(options);z7x3p3.display(); } catch (e) {}};
var scr = d.getElementsByTagName(t)[0], par = scr.parentNode; par.insertBefore(s, scr);
})(document, 'script');</script>
</section>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/2707460527/252a64411a339184ff375a96fb68dcb0_bigger.png">
<em>Mitchell Hashimoto @mitchellh:</em> Docker launched today. It is incredible. Theyre also working RIGHT NOW on a Vagrant provider. LXC is COMING!!
</section>
</div>
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/1108290260/Adam_Jacob-114x150_original_bigger.jpg">
<em>Adam Jacob @adamhjk:</em> Docker is clearly the right idea. @solomonstre absolutely killed it. Containerized app deployment is the future, I think.
</section>
</div>
</div>
<div class="row">
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/14872832/twitter_pic_bigger.jpg">
<em>Matt Townsend @mtownsend:</em> I have a serious code crush on docker.io - it's Lego for PaaS. Motherfucking awesome Lego.
</section>
</div>
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/1312352395/rupert-259x300_bigger.jpg">
<em>Rob Harrop @robertharrop:</em> Impressed by @getdocker - it's all kinds of magic. Serious rethink of AWS architecture happening @skillsmatter.
</section>
</div>
</div>
<div class="row">
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://twimg0-a.akamaihd.net/profile_images/2491994496/rbevyyq6ykp6bnoby2je_bigger.jpeg">
<em>John Willis @botchagalupe:</em> IMHO docker is to paas what chef was to Iaas 4 years ago
</section>
</div>
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://twimg0-a.akamaihd.net/profile_images/3348427561/9d7f08f1e103a16c8debd169301b9944_bigger.jpeg">
<em>John Feminella @superninjarobot:</em> So, @getdocker is pure excellence. If you've ever wished for arbitrary, PaaS-agnostic, lxc/aufs Linux containers, this is your jam!
</section>
</div>
</div>
<div class="row">
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/3408403010/4496ccdd14e9b7285eca04c31a740207_bigger.jpeg">
<em>David Romulan @destructuring:</em> I haven't had this much fun since AWS
</section>
</div>
<div class="span6">
<section class="contentblock twitterblock">
<img src="https://si0.twimg.com/profile_images/780893320/My_Avatar_bigger.jpg">
<em>Ricardo Gladwell @rgladwell:</em> wow @getdocker is either amazing or totally stupid
</section>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span6">
<section class="contentblock">
<h2>Notable features</h2>
<ul>
<li>Filesystem isolation: each process container runs in a completely separate root filesystem.</li>
<li>Resource isolation: system resources like cpu and memory can be allocated differently to each process container, using cgroups.</li>
<li>Network isolation: each process container runs in its own network namespace, with a virtual interface and IP address of its own.</li>
<li>Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremely fast, memory-cheap and disk-cheap.</li>
<li>Logging: the standard streams (stdout/stderr/stdin) of each process container is collected and logged for real-time or batch retrieval.</li>
<li>Change management: changes to a container's filesystem can be committed into a new image and re-used to create more containers. No templating or manual configuration required.</li>
<li>Interactive shell: docker can allocate a pseudo-tty and attach to the standard input of any container, for example to run a throwaway interactive shell.</li>
</ul>
<h2>Under the hood</h2>
<p>Under the hood, Docker is built on the following components:</p>
<ul>
<li>The <a href="http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-24-c">cgroup</a> and <a href="http://blog.dotcloud.com/under-the-hood-linux-kernels-on-dotcloud-part">namespacing</a> capabilities of the Linux kernel;</li>
<li><a href="http://aufs.sourceforge.net/aufs.html">AUFS</a>, a powerful union filesystem with copy-on-write capabilities;</li>
<li>The <a href="http://golang.org">Go</a> programming language;</li>
<li><a href="http://lxc.sourceforge.net/">lxc</a>, a set of convenience scripts to simplify the creation of linux containers.</li>
</ul>
<h2>Who started it</h2>
<p>
Docker is an open-source implementation of the deployment engine which powers <a href="http://dotcloud.com">dotCloud</a>, a popular Platform-as-a-Service.</p>
<p>It benefits directly from the experience accumulated over several years of large-scale operation and support of hundreds of thousands
of applications and databases.
</p>
</section>
</div>
<div class="span6">
<section class="contentblock">
<h3 id="twitter">Twitter</h3>
<a class="twitter-timeline" href="https://twitter.com/getdocker" data-widget-id="312730839718957056">Tweets by @getdocker</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
</section>
</div>
</div>
</div> <!-- end container -->
<div class="container">
<footer id="footer" class="footer">
<div class="row">
<div class="span12">
<div class="tbox textright forceleftmargin social links pull-right">
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
</div>
Docker is a project by <a href="http://www.dotcloud.com">dotCloud</a>
</div>
</div>
<div class="row">
<div class="emptyspace" style="height: 40px">
</div>
</div>
</footer>
</div>
<!-- bootstrap javascipts -->
<script src="static/js/vendor/bootstrap.min.js" type="text/javascript"></script>
<!-- Google analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-6096819-11']);
_gaq.push(['_setDomainName', 'docker.io']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>

View File

@ -1,6 +0,0 @@
# rule to redirect original links created when hosted on github pages
rewrite ^/documentation/(.*).html http://docs.docker.io/en/latest/$1/ permanent;
# rewrite the stuff which was on the current page
rewrite ^/gettingstarted.html$ /gettingstarted/ permanent;

View File

@ -1 +0,0 @@
../theme/docker/static

View File

@ -1,9 +1,7 @@
package docker package docker
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/dotcloud/docker/registry"
"github.com/dotcloud/docker/utils" "github.com/dotcloud/docker/utils"
"io" "io"
"io/ioutil" "io/ioutil"
@ -11,7 +9,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
) )
@ -19,9 +16,6 @@ import (
type Graph struct { type Graph struct {
Root string Root string
idIndex *utils.TruncIndex idIndex *utils.TruncIndex
checksumLock map[string]*sync.Mutex
lockSumFile *sync.Mutex
lockSumMap *sync.Mutex
} }
// NewGraph instantiates a new graph at the given root path in the filesystem. // NewGraph instantiates a new graph at the given root path in the filesystem.
@ -38,9 +32,6 @@ func NewGraph(root string) (*Graph, error) {
graph := &Graph{ graph := &Graph{
Root: abspath, Root: abspath,
idIndex: utils.NewTruncIndex(), idIndex: utils.NewTruncIndex(),
checksumLock: make(map[string]*sync.Mutex),
lockSumFile: &sync.Mutex{},
lockSumMap: &sync.Mutex{},
} }
if err := graph.restore(); err != nil { if err := graph.restore(); err != nil {
return nil, err return nil, err
@ -99,11 +90,6 @@ func (graph *Graph) Get(name string) (*Image, error) {
return nil, err return nil, err
} }
} }
graph.lockSumMap.Lock()
defer graph.lockSumMap.Unlock()
if _, exists := graph.checksumLock[img.ID]; !exists {
graph.checksumLock[img.ID] = &sync.Mutex{}
}
return img, nil return img, nil
} }
@ -123,16 +109,15 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut
img.Container = container.ID img.Container = container.ID
img.ContainerConfig = *container.Config img.ContainerConfig = *container.Config
} }
if err := graph.Register(layerData, layerData != nil, img); err != nil { if err := graph.Register(nil, layerData, img); err != nil {
return nil, err return nil, err
} }
go img.Checksum()
return img, nil return img, nil
} }
// Register imports a pre-existing image into the graph. // Register imports a pre-existing image into the graph.
// FIXME: pass img as first argument // FIXME: pass img as first argument
func (graph *Graph) Register(layerData Archive, store bool, img *Image) error { func (graph *Graph) Register(jsonData []byte, layerData Archive, img *Image) error {
if err := ValidateID(img.ID); err != nil { if err := ValidateID(img.ID); err != nil {
return err return err
} }
@ -145,7 +130,7 @@ func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
if err != nil { if err != nil {
return fmt.Errorf("Mktemp failed: %s", err) return fmt.Errorf("Mktemp failed: %s", err)
} }
if err := StoreImage(img, layerData, tmp, store); err != nil { if err := StoreImage(img, jsonData, layerData, tmp); err != nil {
return err return err
} }
// Commit // Commit
@ -154,7 +139,6 @@ func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
} }
img.graph = graph img.graph = graph
graph.idIndex.Add(img.ID) graph.idIndex.Add(img.ID)
graph.checksumLock[img.ID] = &sync.Mutex{}
return nil return nil
} }
@ -311,40 +295,3 @@ func (graph *Graph) Heads() (map[string]*Image, error) {
func (graph *Graph) imageRoot(id string) string { func (graph *Graph) imageRoot(id string) string {
return path.Join(graph.Root, id) return path.Join(graph.Root, id)
} }
func (graph *Graph) getStoredChecksums() (map[string]string, error) {
checksums := make(map[string]string)
// FIXME: Store the checksum in memory
if checksumDict, err := ioutil.ReadFile(path.Join(graph.Root, "checksums")); err == nil {
if err := json.Unmarshal(checksumDict, &checksums); err != nil {
return nil, err
}
}
return checksums, nil
}
func (graph *Graph) storeChecksums(checksums map[string]string) error {
checksumJSON, err := json.Marshal(checksums)
if err != nil {
return err
}
if err := ioutil.WriteFile(path.Join(graph.Root, "checksums"), checksumJSON, 0600); err != nil {
return err
}
return nil
}
func (graph *Graph) UpdateChecksums(newChecksums map[string]*registry.ImgData) error {
graph.lockSumFile.Lock()
defer graph.lockSumFile.Unlock()
localChecksums, err := graph.getStoredChecksums()
if err != nil {
return err
}
for id, elem := range newChecksums {
localChecksums[id] = elem.Checksum
}
return graph.storeChecksums(localChecksums)
}

View File

@ -38,7 +38,7 @@ func TestInterruptedRegister(t *testing.T) {
Comment: "testing", Comment: "testing",
Created: time.Now(), Created: time.Now(),
} }
go graph.Register(badArchive, false, image) go graph.Register(nil, badArchive, image)
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling) w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling)
if _, err := graph.Get(image.ID); err == nil { if _, err := graph.Get(image.ID); err == nil {
@ -49,7 +49,7 @@ func TestInterruptedRegister(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := graph.Register(goodArchive, false, image); err != nil { if err := graph.Register(nil, goodArchive, image); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@ -95,7 +95,7 @@ func TestRegister(t *testing.T) {
Comment: "testing", Comment: "testing",
Created: time.Now(), Created: time.Now(),
} }
err = graph.Register(archive, false, image) err = graph.Register(nil, archive, image)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -225,7 +225,7 @@ func TestDelete(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Test delete twice (pull -> rm -> pull -> rm) // Test delete twice (pull -> rm -> pull -> rm)
if err := graph.Register(archive, false, img1); err != nil { if err := graph.Register(nil, archive, img1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := graph.Delete(img1.ID); err != nil { if err := graph.Delete(img1.ID); err != nil {

150
image.go
View File

@ -2,7 +2,6 @@ package docker
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -14,6 +13,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -47,6 +47,19 @@ func LoadImage(root string) (*Image, error) {
if err := ValidateID(img.ID); err != nil { if err := ValidateID(img.ID); err != nil {
return nil, err return nil, err
} }
if buf, err := ioutil.ReadFile(path.Join(root, "layersize")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
if size, err := strconv.Atoi(string(buf)); err != nil {
return nil, err
} else {
img.Size = int64(size)
}
}
// Check that the filesystem layer exists // Check that the filesystem layer exists
if stat, err := os.Stat(layerPath(root)); err != nil { if stat, err := os.Stat(layerPath(root)); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -59,7 +72,7 @@ func LoadImage(root string) (*Image, error) {
return img, nil return img, nil
} }
func StoreImage(img *Image, layerData Archive, root string, store bool) error { func StoreImage(img *Image, jsonData []byte, layerData Archive, root string) error {
// Check that root doesn't already exist // Check that root doesn't already exist
if _, err := os.Stat(root); err == nil { if _, err := os.Stat(root); err == nil {
return fmt.Errorf("Image %s already exists", img.ID) return fmt.Errorf("Image %s already exists", img.ID)
@ -72,26 +85,6 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error {
return err return err
} }
if store {
layerArchive := layerArchivePath(root)
file, err := os.OpenFile(layerArchive, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
// FIXME: Retrieve the image layer size from here?
if _, err := io.Copy(file, layerData); err != nil {
return err
}
// FIXME: Don't close/open, read/write instead of Copy
file.Close()
file, err = os.Open(layerArchive)
if err != nil {
return err
}
defer file.Close()
layerData = file
}
// If layerData is not nil, unpack it into the new layer // If layerData is not nil, unpack it into the new layer
if layerData != nil { if layerData != nil {
start := time.Now() start := time.Now()
@ -102,18 +95,10 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error {
utils.Debugf("Untar time: %vs\n", time.Now().Sub(start).Seconds()) utils.Debugf("Untar time: %vs\n", time.Now().Sub(start).Seconds())
} }
return StoreSize(img, root) // If raw json is provided, then use it
} if jsonData != nil {
return ioutil.WriteFile(jsonPath(root), jsonData, 0600)
func StoreSize(img *Image, root string) error { } else { // Otherwise, unmarshal the image
layer := layerPath(root)
filepath.Walk(layer, func(path string, fileInfo os.FileInfo, err error) error {
img.Size += fileInfo.Size()
return nil
})
// Store the json ball
jsonData, err := json.Marshal(img) jsonData, err := json.Marshal(img)
if err != nil { if err != nil {
return err return err
@ -121,6 +106,25 @@ func StoreSize(img *Image, root string) error {
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil { if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
return err return err
} }
}
return StoreSize(img, root)
}
func StoreSize(img *Image, root string) error {
layer := layerPath(root)
var totalSize int64 = 0
filepath.Walk(layer, func(path string, fileInfo os.FileInfo, err error) error {
totalSize += fileInfo.Size()
return nil
})
img.Size = totalSize
if err := ioutil.WriteFile(path.Join(root, "layersize"), []byte(strconv.Itoa(int(totalSize))), 0600); err != nil {
return nil
}
return nil return nil
} }
@ -128,10 +132,6 @@ func layerPath(root string) string {
return path.Join(root, "layer") return path.Join(root, "layer")
} }
func layerArchivePath(root string) string {
return path.Join(root, "layer.tar.xz")
}
func jsonPath(root string) string { func jsonPath(root string) string {
return path.Join(root, "json") return path.Join(root, "json")
} }
@ -308,80 +308,6 @@ func (img *Image) layer() (string, error) {
return layerPath(root), nil return layerPath(root), nil
} }
func (img *Image) Checksum() (string, error) {
img.graph.checksumLock[img.ID].Lock()
defer img.graph.checksumLock[img.ID].Unlock()
root, err := img.root()
if err != nil {
return "", err
}
checksums, err := img.graph.getStoredChecksums()
if err != nil {
return "", err
}
if checksum, ok := checksums[img.ID]; ok {
return checksum, nil
}
layer, err := img.layer()
if err != nil {
return "", err
}
jsonData, err := ioutil.ReadFile(jsonPath(root))
if err != nil {
return "", err
}
var layerData io.Reader
if file, err := os.Open(layerArchivePath(root)); err != nil {
if os.IsNotExist(err) {
layerData, err = Tar(layer, Xz)
if err != nil {
return "", err
}
} else {
return "", err
}
} else {
defer file.Close()
layerData = file
}
h := sha256.New()
if _, err := h.Write(jsonData); err != nil {
return "", err
}
if _, err := h.Write([]byte("\n")); err != nil {
return "", err
}
if _, err := io.Copy(h, layerData); err != nil {
return "", err
}
hash := "sha256:" + hex.EncodeToString(h.Sum(nil))
// Reload the json file to make sure not to overwrite faster sums
img.graph.lockSumFile.Lock()
defer img.graph.lockSumFile.Unlock()
checksums, err = img.graph.getStoredChecksums()
if err != nil {
return "", err
}
checksums[img.ID] = hash
// Dump the checksums to disc
if err := img.graph.storeChecksums(checksums); err != nil {
return hash, err
}
return hash, nil
}
func (img *Image) getParentsSize(size int64) int64 { func (img *Image) getParentsSize(size int64) int64 {
parentImage, err := img.GetParent() parentImage, err := img.GetParent()
if err != nil || parentImage == nil { if err != nil || parentImage == nil {

View File

@ -1,9 +1,8 @@
description "Run docker" description "Run docker"
start on runlevel [2345] start on filesystem or runlevel [2345]
stop on starting rc RUNLEVEL=[016] stop on runlevel [!2345]
respawn respawn
script exec /usr/bin/docker -d
/usr/bin/docker -d
end script

View File

@ -17,8 +17,10 @@ import (
"strings" "strings"
) )
var ErrAlreadyExists = errors.New("Image already exists") var (
var ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") ErrAlreadyExists = errors.New("Image already exists")
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
)
func pingRegistryEndpoint(endpoint string) error { func pingRegistryEndpoint(endpoint string) error {
if endpoint == auth.IndexServerAddress() { if endpoint == auth.IndexServerAddress() {
@ -109,7 +111,14 @@ func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
for _, cookie := range c.Jar.Cookies(req.URL) { for _, cookie := range c.Jar.Cookies(req.URL) {
req.AddCookie(cookie) req.AddCookie(cookie)
} }
return c.Do(req) res, err := c.Do(req)
if err != nil {
return nil, err
}
if len(res.Cookies()) > 0 {
c.Jar.SetCookies(req.URL, res.Cookies())
}
return res, err
} }
// Set the user agent field in the header based on the versions provided // Set the user agent field in the header based on the versions provided
@ -135,10 +144,10 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s
} }
req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
r.setUserAgent(req) r.setUserAgent(req)
res, err := r.client.Do(req) res, err := doWithCookies(r.client, req)
if err != nil || res.StatusCode != 200 { if err != nil || res.StatusCode != 200 {
if res != nil { if res != nil {
return nil, fmt.Errorf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID) return nil, utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res)
} }
return nil, err return nil, err
} }
@ -182,13 +191,13 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([
} }
req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
r.setUserAgent(req) r.setUserAgent(req)
res, err := r.client.Do(req) res, err := doWithCookies(r.client, req)
if err != nil { if err != nil {
return nil, -1, fmt.Errorf("Failed to download json: %s", err) return nil, -1, fmt.Errorf("Failed to download json: %s", err)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return nil, -1, fmt.Errorf("HTTP code %d", res.StatusCode) return nil, -1, utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res)
} }
imageSize, err := strconv.Atoi(res.Header.Get("X-Docker-Size")) imageSize, err := strconv.Atoi(res.Header.Get("X-Docker-Size"))
@ -210,7 +219,7 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) (
} }
req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
r.setUserAgent(req) r.setUserAgent(req)
res, err := r.client.Do(req) res, err := doWithCookies(r.client, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -231,7 +240,7 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
} }
req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
r.setUserAgent(req) r.setUserAgent(req)
res, err := r.client.Do(req) res, err := doWithCookies(r.client, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -259,8 +268,11 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
} }
func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, error) { func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, error) {
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote) repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote)
utils.Debugf("[registry] Calling GET %s", repositoryTarget)
req, err := r.opaqueRequest("GET", repositoryTarget, nil) req, err := r.opaqueRequest("GET", repositoryTarget, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -277,12 +289,12 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode == 401 { if res.StatusCode == 401 {
return nil, fmt.Errorf("Please login first (HTTP code %d)", res.StatusCode) return nil, utils.NewHTTPRequestError(fmt.Sprintf("Please login first (HTTP code %d)", res.StatusCode), res)
} }
// TODO: Right now we're ignoring checksums in the response body. // TODO: Right now we're ignoring checksums in the response body.
// In the future, we need to use them to check image validity. // In the future, we need to use them to check image validity.
if res.StatusCode != 200 { if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP code: %d", res.StatusCode) return nil, utils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res)
} }
var tokens []string var tokens []string
@ -323,19 +335,17 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
}, nil }, nil
} }
// Push a local image to the registry func (r *Registry) PushImageChecksumRegistry(imgData *ImgData, registry string, token []string) error {
func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string, token []string) error {
// FIXME: try json with UTF8 utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum")
req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", strings.NewReader(string(jsonRaw)))
req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil)
if err != nil { if err != nil {
return err return err
} }
req.Header.Add("Content-type", "application/json")
req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
req.Header.Set("X-Docker-Checksum", imgData.Checksum) req.Header.Set("X-Docker-Checksum", imgData.Checksum)
r.setUserAgent(req)
utils.Debugf("Setting checksum for %s: %s", imgData.ID, imgData.Checksum)
res, err := doWithCookies(r.client, req) res, err := doWithCookies(r.client, req)
if err != nil { if err != nil {
return fmt.Errorf("Failed to upload metadata: %s", err) return fmt.Errorf("Failed to upload metadata: %s", err)
@ -360,29 +370,68 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis
return nil return nil
} }
func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, token []string) error { // Push a local image to the registry
req, err := http.NewRequest("PUT", registry+"images/"+imgID+"/layer", layer) func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string, token []string) error {
utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/json")
req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", bytes.NewReader(jsonRaw))
if err != nil { if err != nil {
return err return err
} }
req.Header.Add("Content-type", "application/json")
req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
r.setUserAgent(req)
res, err := doWithCookies(r.client, req)
if err != nil {
return fmt.Errorf("Failed to upload metadata: %s", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
errBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return utils.NewHTTPRequestError(fmt.Sprint("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res)
}
var jsonBody map[string]string
if err := json.Unmarshal(errBody, &jsonBody); err != nil {
errBody = []byte(err.Error())
} else if jsonBody["error"] == "Image already exists" {
return ErrAlreadyExists
}
return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody), res)
}
return nil
}
func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, token []string, jsonRaw []byte) (checksum string, err error) {
utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgID+"/layer")
tarsumLayer := &utils.TarSum{Reader: layer}
req, err := http.NewRequest("PUT", registry+"images/"+imgID+"/layer", tarsumLayer)
if err != nil {
return "", err
}
req.ContentLength = -1 req.ContentLength = -1
req.TransferEncoding = []string{"chunked"} req.TransferEncoding = []string{"chunked"}
req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
r.setUserAgent(req) r.setUserAgent(req)
res, err := doWithCookies(r.client, req) res, err := doWithCookies(r.client, req)
if err != nil { if err != nil {
return fmt.Errorf("Failed to upload layer: %s", err) return "", fmt.Errorf("Failed to upload layer: %s", err)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
errBody, err := ioutil.ReadAll(res.Body) errBody, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err) return "", utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res)
} }
return fmt.Errorf("Received HTTP code %d while uploading layer: %s", res.StatusCode, errBody) return "", utils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %s", res.StatusCode, errBody), res)
} }
return nil return tarsumLayer.Sum(jsonRaw), nil
} }
func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.Request, error) { func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
@ -414,13 +463,25 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token
} }
res.Body.Close() res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 { 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) return utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote), res)
} }
return nil return nil
} }
func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) {
imgListJSON, err := json.Marshal(imgList) cleanImgList := []*ImgData{}
if validate {
for _, elem := range imgList {
if elem.Checksum != "" {
cleanImgList = append(cleanImgList, elem)
}
}
} else {
cleanImgList = imgList
}
imgListJSON, err := json.Marshal(cleanImgList)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -430,7 +491,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
} }
u := fmt.Sprintf("%srepositories/%s/%s", indexEp, remote, suffix) u := fmt.Sprintf("%srepositories/%s/%s", indexEp, remote, suffix)
utils.Debugf("PUT %s", u) utils.Debugf("[registry] PUT %s", u)
utils.Debugf("Image list pushed to index:\n%s\n", imgListJSON) utils.Debugf("Image list pushed to index:\n%s\n", imgListJSON)
req, err := r.opaqueRequest("PUT", u, bytes.NewReader(imgListJSON)) req, err := r.opaqueRequest("PUT", u, bytes.NewReader(imgListJSON))
if err != nil { if err != nil {
@ -479,7 +540,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, fmt.Errorf("Error: Status %d trying to push repository %s: %s", res.StatusCode, remote, errBody) return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %s", res.StatusCode, remote, errBody), res)
} }
if res.Header.Get("X-Docker-Token") != "" { if res.Header.Get("X-Docker-Token") != "" {
tokens = res.Header["X-Docker-Token"] tokens = res.Header["X-Docker-Token"]
@ -503,7 +564,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, fmt.Errorf("Error: Status %d trying to push checksums %s: %s", res.StatusCode, remote, errBody) return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %s", res.StatusCode, remote, errBody), res)
} }
} }
@ -525,7 +586,7 @@ func (r *Registry) SearchRepositories(term string) (*SearchResults, error) {
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return nil, fmt.Errorf("Unexepected status code %d", res.StatusCode) return nil, utils.NewHTTPRequestError(fmt.Sprintf("Unexepected status code %d", res.StatusCode), res)
} }
rawData, err := ioutil.ReadAll(res.Body) rawData, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {

150
server.go
View File

@ -309,35 +309,34 @@ func (srv *Server) ImageHistory(name string) ([]APIHistory, error) {
} }
func (srv *Server) ContainerTop(name string) ([]APITop, error) { func (srv *Server) ContainerTop(name, ps_args string) (*APITop, error) {
if container := srv.runtime.Get(name); container != nil { if container := srv.runtime.Get(name); container != nil {
output, err := exec.Command("lxc-ps", "--name", container.ID).CombinedOutput() output, err := exec.Command("lxc-ps", "--name", container.ID, "--", ps_args).CombinedOutput()
if err != nil { if err != nil {
return nil, fmt.Errorf("Error trying to use lxc-ps: %s (%s)", err, output) return nil, fmt.Errorf("Error trying to use lxc-ps: %s (%s)", err, output)
} }
var procs []APITop procs := APITop{}
for i, line := range strings.Split(string(output), "\n") { for i, line := range strings.Split(string(output), "\n") {
if i == 0 || len(line) == 0 { if len(line) == 0 {
continue continue
} }
proc := APITop{} words := []string{}
scanner := bufio.NewScanner(strings.NewReader(line)) scanner := bufio.NewScanner(strings.NewReader(line))
scanner.Split(bufio.ScanWords) scanner.Split(bufio.ScanWords)
if !scanner.Scan() { if !scanner.Scan() {
return nil, fmt.Errorf("Error trying to use lxc-ps") return nil, fmt.Errorf("Error trying to use lxc-ps")
} }
// no scanner.Text because we skip container id // no scanner.Text because we skip container id
scanner.Scan() for scanner.Scan() {
proc.PID = scanner.Text() words = append(words, scanner.Text())
scanner.Scan()
proc.Tty = scanner.Text()
scanner.Scan()
proc.Time = scanner.Text()
scanner.Scan()
proc.Cmd = scanner.Text()
procs = append(procs, proc)
} }
return procs, nil if i == 0 {
procs.Titles = words
} else {
procs.Processes = append(procs.Processes, words)
}
}
return &procs, nil
} }
return nil, fmt.Errorf("No such container: %s", name) return nil, fmt.Errorf("No such container: %s", name)
@ -439,7 +438,7 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgID, endpoin
return err return err
} }
defer layer.Close() defer layer.Close()
if err := srv.runtime.graph.Register(utils.ProgressReader(layer, imgSize, out, sf.FormatProgress("Downloading", "%8v/%v (%v)"), sf), false, img); err != nil { if err := srv.runtime.graph.Register(imgJSON, utils.ProgressReader(layer, imgSize, out, sf.FormatProgress("Downloading", "%8v/%v (%v)"), sf), img); err != nil {
return err return err
} }
} }
@ -455,12 +454,6 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, localName
return err return err
} }
utils.Debugf("Updating checksums")
// Reload the json file to make sure not to overwrite faster sums
if err := srv.runtime.graph.UpdateChecksums(repoData.ImgList); err != nil {
return err
}
utils.Debugf("Retrieving the tag list") utils.Debugf("Retrieving the tag list")
tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens) tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens)
if err != nil { if err != nil {
@ -598,41 +591,6 @@ func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *ut
return nil return nil
} }
// Retrieve the checksum of an image
// Priority:
// - Check on the stored checksums
// - Check if the archive exists, if it does not, ask the registry
// - If the archive does exists, process the checksum from it
// - If the archive does not exists and not found on registry, process checksum from layer
func (srv *Server) getChecksum(imageID string) (string, error) {
// FIXME: Use in-memory map instead of reading the file each time
if sums, err := srv.runtime.graph.getStoredChecksums(); err != nil {
return "", err
} else if checksum, exists := sums[imageID]; exists {
return checksum, nil
}
img, err := srv.runtime.graph.Get(imageID)
if err != nil {
return "", err
}
if _, err := os.Stat(layerArchivePath(srv.runtime.graph.imageRoot(imageID))); err != nil {
if os.IsNotExist(err) {
// TODO: Ask the registry for the checksum
// As the archive is not there, it is supposed to come from a pull.
} else {
return "", err
}
}
checksum, err := img.Checksum()
if err != nil {
return "", err
}
return checksum, nil
}
// Retrieve the all the images to be uploaded in the correct order // Retrieve the all the images to be uploaded in the correct order
// Note: we can't use a map as it is not ordered // Note: we can't use a map as it is not ordered
func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgData, error) { func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgData, error) {
@ -649,13 +607,9 @@ func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgDat
return nil return nil
} }
imageSet[img.ID] = struct{}{} imageSet[img.ID] = struct{}{}
checksum, err := srv.getChecksum(img.ID)
if err != nil {
return err
}
imgList = append([]*registry.ImgData{{ imgList = append([]*registry.ImgData{{
ID: img.ID, ID: img.ID,
Checksum: checksum,
Tag: tag, Tag: tag,
}}, imgList...) }}, imgList...)
return nil return nil
@ -666,7 +620,7 @@ func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgDat
func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, localName, remoteName string, localRepo map[string]string, indexEp string, sf *utils.StreamFormatter) error { func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, localName, remoteName string, localRepo map[string]string, indexEp string, sf *utils.StreamFormatter) error {
out = utils.NewWriteFlusher(out) out = utils.NewWriteFlusher(out)
out.Write(sf.FormatStatus("Processing checksums"))
imgList, err := srv.getImageList(localRepo) imgList, err := srv.getImageList(localRepo)
if err != nil { if err != nil {
return err return err
@ -690,9 +644,11 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, localName
out.Write(sf.FormatStatus("Image %s already pushed, skipping", elem.ID)) out.Write(sf.FormatStatus("Image %s already pushed, skipping", elem.ID))
continue continue
} }
if err := srv.pushImage(r, out, remoteName, elem.ID, ep, repoData.Tokens, sf); err != nil { if checksum, err := srv.pushImage(r, out, remoteName, elem.ID, ep, repoData.Tokens, sf); err != nil {
// FIXME: Continue on error? // FIXME: Continue on error?
return err return err
} else {
elem.Checksum = checksum
} }
out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"repositories/"+remoteName+"/tags/"+elem.Tag)) out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"repositories/"+remoteName+"/tags/"+elem.Tag))
if err := r.PushRegistryTag(remoteName, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil { if err := r.PushRegistryTag(remoteName, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil {
@ -708,64 +664,45 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, localName
return nil return nil
} }
func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgID, ep string, token []string, sf *utils.StreamFormatter) error { func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) {
out = utils.NewWriteFlusher(out) out = utils.NewWriteFlusher(out)
jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgID, "json")) jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgID, "json"))
if err != nil { if err != nil {
return fmt.Errorf("Error while retreiving the path for {%s}: %s", imgID, err) return "", fmt.Errorf("Error while retreiving the path for {%s}: %s", imgID, err)
} }
out.Write(sf.FormatStatus("Pushing %s", imgID)) out.Write(sf.FormatStatus("Pushing %s", imgID))
// Make sure we have the image's checksum
checksum, err := srv.getChecksum(imgID)
if err != nil {
return err
}
imgData := &registry.ImgData{ imgData := &registry.ImgData{
ID: imgID, ID: imgID,
Checksum: checksum,
} }
// Send the json // Send the json
if err := r.PushImageJSONRegistry(imgData, jsonRaw, ep, token); err != nil { if err := r.PushImageJSONRegistry(imgData, jsonRaw, ep, token); err != nil {
if err == registry.ErrAlreadyExists { if err == registry.ErrAlreadyExists {
out.Write(sf.FormatStatus("Image %s already pushed, skipping", imgData.ID)) out.Write(sf.FormatStatus("Image %s already pushed, skipping", imgData.ID))
return nil return "", nil
} }
return err return "", err
} }
// Retrieve the tarball to be sent layerData, err := srv.runtime.graph.TempLayerArchive(imgID, Uncompressed, sf, out)
var layerData *TempArchive
// If the archive exists, use it
file, err := os.Open(layerArchivePath(srv.runtime.graph.imageRoot(imgID)))
if err != nil { if err != nil {
if os.IsNotExist(err) { return "", fmt.Errorf("Failed to generate layer archive: %s", err)
// If the archive does not exist, create one from the layer
layerData, err = srv.runtime.graph.TempLayerArchive(imgID, Xz, sf, out)
if err != nil {
return fmt.Errorf("Failed to generate layer archive: %s", err)
}
} else {
return err
}
} else {
defer file.Close()
st, err := file.Stat()
if err != nil {
return err
}
layerData = &TempArchive{
File: file,
Size: st.Size(),
}
} }
// Send the layer // Send the layer
if err := r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf.FormatProgress("Pushing", "%8v/%v (%v)"), sf), ep, token); err != nil { if checksum, err := r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf.FormatProgress("Pushing", "%8v/%v (%v)"), sf), ep, token, jsonRaw); err != nil {
return err return "", err
} else {
imgData.Checksum = checksum
} }
return nil
// Send the checksum
if err := r.PushImageChecksumRegistry(imgData, ep, token); err != nil {
return "", err
}
return imgData.Checksum, nil
} }
// FIXME: Allow to interupt current push when new push of same image is done. // FIXME: Allow to interupt current push when new push of same image is done.
@ -803,7 +740,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo
var token []string var token []string
out.Write(sf.FormatStatus("The push refers to an image: [%s]", localName)) out.Write(sf.FormatStatus("The push refers to an image: [%s]", localName))
if err := srv.pushImage(r, out, remoteName, img.ID, endpoint, token, sf); err != nil { if _, err := srv.pushImage(r, out, remoteName, img.ID, endpoint, token, sf); err != nil {
return err return err
} }
return nil return nil
@ -995,6 +932,9 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro
parsedRepo := strings.Split(repoAndTag, ":")[0] parsedRepo := strings.Split(repoAndTag, ":")[0]
if strings.Contains(img.ID, repoName) { if strings.Contains(img.ID, repoName) {
repoName = parsedRepo repoName = parsedRepo
if len(srv.runtime.repositories.ByID()[img.ID]) == 1 && len(strings.Split(repoAndTag, ":")) > 1 {
tag = strings.Split(repoAndTag, ":")[1]
}
} else if repoName != parsedRepo { } else if repoName != parsedRepo {
// the id belongs to multiple repos, like base:latest and user:test, // the id belongs to multiple repos, like base:latest and user:test,
// in that case return conflict // in that case return conflict
@ -1037,13 +977,7 @@ func (srv *Server) ImageDelete(name string, autoPrune bool) ([]APIRmi, error) {
return nil, nil return nil, nil
} }
var tag string name, tag := utils.ParseRepositoryTag(name)
if strings.Contains(name, ":") {
nameParts := strings.Split(name, ":")
name = nameParts[0]
tag = nameParts[1]
}
return srv.deleteImage(img, name, tag) return srv.deleteImage(img, name, tag)
} }

View File

@ -2,6 +2,7 @@ package docker
import ( import (
"github.com/dotcloud/docker/utils" "github.com/dotcloud/docker/utils"
"strings"
"testing" "testing"
"time" "time"
) )
@ -20,16 +21,20 @@ func TestContainerTagImageDelete(t *testing.T) {
if err := srv.runtime.repositories.Set("utest", "tag1", unitTestImageName, false); err != nil { if err := srv.runtime.repositories.Set("utest", "tag1", unitTestImageName, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := srv.runtime.repositories.Set("utest/docker", "tag2", unitTestImageName, false); err != nil { if err := srv.runtime.repositories.Set("utest/docker", "tag2", unitTestImageName, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := srv.runtime.repositories.Set("utest:5000/docker", "tag3", unitTestImageName, false); err != nil {
t.Fatal(err)
}
images, err := srv.Images(false, "") images, err := srv.Images(false, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(images) != len(initialImages)+2 { if len(images) != len(initialImages)+3 {
t.Errorf("Expected %d images, %d found", len(initialImages)+2, len(images)) t.Errorf("Expected %d images, %d found", len(initialImages)+2, len(images))
} }
@ -42,6 +47,19 @@ func TestContainerTagImageDelete(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if len(images) != len(initialImages)+2 {
t.Errorf("Expected %d images, %d found", len(initialImages)+2, len(images))
}
if _, err := srv.ImageDelete("utest:5000/docker:tag3", true); err != nil {
t.Fatal(err)
}
images, err = srv.Images(false, "")
if err != nil {
t.Fatal(err)
}
if len(images) != len(initialImages)+1 { if len(images) != len(initialImages)+1 {
t.Errorf("Expected %d images, %d found", len(initialImages)+1, len(images)) t.Errorf("Expected %d images, %d found", len(initialImages)+1, len(images))
} }
@ -90,6 +108,27 @@ func TestCreateRm(t *testing.T) {
} }
func TestCommit(t *testing.T) {
runtime := mkRuntime(t)
defer nuke(runtime)
srv := &Server{runtime: runtime}
config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "/bin/cat"}, nil)
if err != nil {
t.Fatal(err)
}
id, err := srv.ContainerCreate(config)
if err != nil {
t.Fatal(err)
}
if _, err := srv.ContainerCommit(id, "testrepo", "testtag", "", "", config); err != nil {
t.Fatal(err)
}
}
func TestCreateStartRestartStopStartKillRm(t *testing.T) { func TestCreateStartRestartStopStartKillRm(t *testing.T) {
runtime := mkRuntime(t) runtime := mkRuntime(t)
defer nuke(runtime) defer nuke(runtime)
@ -203,3 +242,88 @@ func TestLogEvent(t *testing.T) {
}) })
} }
func TestRmi(t *testing.T) {
runtime := mkRuntime(t)
defer nuke(runtime)
srv := &Server{runtime: runtime}
initialImages, err := srv.Images(false, "")
if err != nil {
t.Fatal(err)
}
config, hostConfig, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil)
if err != nil {
t.Fatal(err)
}
containerID, err := srv.ContainerCreate(config)
if err != nil {
t.Fatal(err)
}
//To remove
err = srv.ContainerStart(containerID, hostConfig)
if err != nil {
t.Fatal(err)
}
imageID, err := srv.ContainerCommit(containerID, "test", "", "", "", nil)
if err != nil {
t.Fatal(err)
}
err = srv.ContainerTag(imageID, "test", "0.1", false)
if err != nil {
t.Fatal(err)
}
containerID, err = srv.ContainerCreate(config)
if err != nil {
t.Fatal(err)
}
//To remove
err = srv.ContainerStart(containerID, hostConfig)
if err != nil {
t.Fatal(err)
}
_, err = srv.ContainerCommit(containerID, "test", "", "", "", nil)
if err != nil {
t.Fatal(err)
}
images, err := srv.Images(false, "")
if err != nil {
t.Fatal(err)
}
if len(images)-len(initialImages) != 2 {
t.Fatalf("Expected 2 new images, found %d.", len(images)-len(initialImages))
}
_, err = srv.ImageDelete(imageID, true)
if err != nil {
t.Fatal(err)
}
images, err = srv.Images(false, "")
if err != nil {
t.Fatal(err)
}
if len(images)-len(initialImages) != 1 {
t.Fatalf("Expected 1 new image, found %d.", len(images)-len(initialImages))
}
for _, image := range images {
if strings.Contains(unitTestImageID, image.ID) {
continue
}
if image.Repository == "" {
t.Fatalf("Expected tagged image, got untagged one.")
}
}
}

View File

@ -40,6 +40,10 @@ Deployment
export SMTP_USER=xxxxxxxxxxxx export SMTP_USER=xxxxxxxxxxxx
export SMTP_PWD=xxxxxxxxxxxx export SMTP_PWD=xxxxxxxxxxxx
# Define docker registry functional test credentials
export REGISTRY_USER=xxxxxxxxxxxx
export REGISTRY_PWD=xxxxxxxxxxxx
# Checkout docker # Checkout docker
git clone git://github.com/dotcloud/docker.git git clone git://github.com/dotcloud/docker.git

4
testing/Vagrantfile vendored
View File

@ -29,7 +29,9 @@ Vagrant::Config.run do |config|
"chown #{USER}.#{USER} /data; cd /data; " \ "chown #{USER}.#{USER} /data; cd /data; " \
"#{CFG_PATH}/setup.sh #{USER} #{CFG_PATH} #{ENV['BUILDBOT_PWD']} " \ "#{CFG_PATH}/setup.sh #{USER} #{CFG_PATH} #{ENV['BUILDBOT_PWD']} " \
"#{ENV['IRC_PWD']} #{ENV['IRC_CHANNEL']} #{ENV['SMTP_USER']} " \ "#{ENV['IRC_PWD']} #{ENV['IRC_CHANNEL']} #{ENV['SMTP_USER']} " \
"#{ENV['SMTP_PWD']} #{ENV['EMAIL_RCP']}; " "#{ENV['SMTP_PWD']} #{ENV['EMAIL_RCP']}; " \
"#{CFG_PATH}/setup_credentials.sh #{USER} " \
"#{ENV['REGISTRY_USER']} #{ENV['REGISTRY_PWD']}; "
# Install docker dependencies # Install docker dependencies
pkg_cmd << "apt-get install -q -y python-software-properties; " \ pkg_cmd << "apt-get install -q -y python-software-properties; " \
"add-apt-repository -y ppa:dotcloud/docker-golang/ubuntu; apt-get update -qq; " \ "add-apt-repository -y ppa:dotcloud/docker-golang/ubuntu; apt-get update -qq; " \

View File

@ -0,0 +1,5 @@
# Credentials for tests. Buildbot source this file on tests
# when needed.
# Docker registry credentials. Format: 'username:password'
export DOCKER_CREDS=''

View File

@ -19,6 +19,7 @@ TEST_USER = 'buildbot' # Credential to authenticate build triggers
TEST_PWD = 'docker' # Credential to authenticate build triggers TEST_PWD = 'docker' # Credential to authenticate build triggers
BUILDER_NAME = 'docker' BUILDER_NAME = 'docker'
GITHUB_DOCKER = 'github.com/dotcloud/docker' GITHUB_DOCKER = 'github.com/dotcloud/docker'
BUILDBOT_PATH = '/data/buildbot'
DOCKER_PATH = '/data/docker' DOCKER_PATH = '/data/docker'
BUILDER_PATH = '/data/buildbot/slave/{0}/build'.format(BUILDER_NAME) BUILDER_PATH = '/data/buildbot/slave/{0}/build'.format(BUILDER_NAME)
DOCKER_BUILD_PATH = BUILDER_PATH + '/src/github.com/dotcloud/docker' DOCKER_BUILD_PATH = BUILDER_PATH + '/src/github.com/dotcloud/docker'
@ -41,16 +42,19 @@ c['db'] = {'db_url':"sqlite:///state.sqlite"}
c['slaves'] = [BuildSlave('buildworker', BUILDBOT_PWD)] c['slaves'] = [BuildSlave('buildworker', BUILDBOT_PWD)]
c['slavePortnum'] = PORT_MASTER c['slavePortnum'] = PORT_MASTER
# Schedulers # Schedulers
c['schedulers'] = [ForceScheduler(name='trigger', builderNames=[BUILDER_NAME, c['schedulers'] = [ForceScheduler(name='trigger', builderNames=[BUILDER_NAME,
'coverage'])] 'registry','coverage'])]
c['schedulers'] += [SingleBranchScheduler(name="all", c['schedulers'] += [SingleBranchScheduler(name="all",
change_filter=filter.ChangeFilter(branch='master'), treeStableTimer=None, change_filter=filter.ChangeFilter(branch='master'), treeStableTimer=None,
builderNames=[BUILDER_NAME])] builderNames=[BUILDER_NAME])]
c['schedulers'] += [Nightly(name='daily', branch=None, builderNames=['coverage'], c['schedulers'] += [Nightly(name='daily', branch=None, builderNames=['coverage','registry'],
hour=0, minute=30)] hour=0, minute=30)]
# Builders # Builders
# Docker commit test
factory = BuildFactory() factory = BuildFactory()
factory.addStep(ShellCommand(description='Docker',logEnviron=False,usePTY=True, factory.addStep(ShellCommand(description='Docker',logEnviron=False,usePTY=True,
command=["sh", "-c", Interpolate("cd ..; rm -rf build; export GOPATH={0}; " command=["sh", "-c", Interpolate("cd ..; rm -rf build; export GOPATH={0}; "
@ -58,6 +62,7 @@ factory.addStep(ShellCommand(description='Docker',logEnviron=False,usePTY=True,
"go test -v".format(BUILDER_PATH,GITHUB_DOCKER,DOCKER_BUILD_PATH))])) "go test -v".format(BUILDER_PATH,GITHUB_DOCKER,DOCKER_BUILD_PATH))]))
c['builders'] = [BuilderConfig(name=BUILDER_NAME,slavenames=['buildworker'], c['builders'] = [BuilderConfig(name=BUILDER_NAME,slavenames=['buildworker'],
factory=factory)] factory=factory)]
# Docker coverage test # Docker coverage test
coverage_cmd = ('GOPATH=`pwd` go get -d github.com/dotcloud/docker\n' coverage_cmd = ('GOPATH=`pwd` go get -d github.com/dotcloud/docker\n'
'GOPATH=`pwd` go get github.com/axw/gocov/gocov\n' 'GOPATH=`pwd` go get github.com/axw/gocov/gocov\n'
@ -69,6 +74,17 @@ factory.addStep(ShellCommand(description='Coverage',logEnviron=False,usePTY=True
c['builders'] += [BuilderConfig(name='coverage',slavenames=['buildworker'], c['builders'] += [BuilderConfig(name='coverage',slavenames=['buildworker'],
factory=factory)] factory=factory)]
# Registry Functionaltest builder
factory = BuildFactory()
factory.addStep(ShellCommand(description='registry', logEnviron=False,
command='. {0}/master/credentials.cfg; '
'{1}/testing/functionaltests/test_registry.sh'.format(BUILDBOT_PATH,
DOCKER_PATH), usePTY=True))
c['builders'] += [BuilderConfig(name='registry',slavenames=['buildworker'],
factory=factory)]
# Status # Status
authz_cfg = authz.Authz(auth=auth.BasicAuth([(TEST_USER, TEST_PWD)]), authz_cfg = authz.Authz(auth=auth.BasicAuth([(TEST_USER, TEST_PWD)]),
forceBuild='auth') forceBuild='auth')

View File

@ -4,3 +4,4 @@ buildbot==0.8.7p1
buildbot_slave==0.8.7p1 buildbot_slave==0.8.7p1
nose==1.2.1 nose==1.2.1
requests==1.1.0 requests==1.1.0
flask==0.10.1

View File

@ -0,0 +1,17 @@
#!/bin/bash
# Setup of test credentials. Called by Vagrantfile
export PATH="/bin:sbin:/usr/bin:/usr/sbin:/usr/local/bin"
USER=$1
REGISTRY_USER=$2
REGISTRY_PWD=$3
BUILDBOT_PATH="/data/buildbot"
DOCKER_PATH="/data/docker"
function run { su $USER -c "$1"; }
run "cp $DOCKER_PATH/testing/buildbot/credentials.cfg $BUILDBOT_PATH/master"
cd $BUILDBOT_PATH/master
run "sed -i -E 's#(export DOCKER_CREDS=).+#\1\"$REGISTRY_USER:$REGISTRY_PWD\"#' credentials.cfg"

View File

@ -0,0 +1,11 @@
#!/bin/sh
# Cleanup
rm -rf docker-registry
# Get latest docker registry
git clone https://github.com/dotcloud/docker-registry.git
# Configure and run registry tests
cd docker-registry; cp config_sample.yml config.yml
cd test; python -m unittest workflow

158
utils/tarsum.go Normal file
View File

@ -0,0 +1,158 @@
package utils
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"hash"
"io"
"sort"
"strconv"
)
type verboseHash struct {
hash.Hash
}
func (h verboseHash) Write(buf []byte) (int, error) {
Debugf("--->%s<---", buf)
return h.Hash.Write(buf)
}
type TarSum struct {
io.Reader
tarR *tar.Reader
tarW *tar.Writer
gz *gzip.Writer
bufTar *bytes.Buffer
bufGz *bytes.Buffer
h hash.Hash
h2 verboseHash
sums []string
finished bool
first bool
}
func (ts *TarSum) encodeHeader(h *tar.Header) error {
for _, elem := range [][2]string{
{"name", h.Name},
{"mode", strconv.Itoa(int(h.Mode))},
{"uid", strconv.Itoa(h.Uid)},
{"gid", strconv.Itoa(h.Gid)},
{"size", strconv.Itoa(int(h.Size))},
{"mtime", strconv.Itoa(int(h.ModTime.UTC().Unix()))},
{"typeflag", string([]byte{h.Typeflag})},
{"linkname", h.Linkname},
{"uname", h.Uname},
{"gname", h.Gname},
{"devmajor", strconv.Itoa(int(h.Devmajor))},
{"devminor", strconv.Itoa(int(h.Devminor))},
// {"atime", strconv.Itoa(int(h.AccessTime.UTC().Unix()))},
// {"ctime", strconv.Itoa(int(h.ChangeTime.UTC().Unix()))},
} {
// Debugf("-->%s<-- -->%s<--", elem[0], elem[1])
if _, err := ts.h.Write([]byte(elem[0] + elem[1])); err != nil {
return err
}
}
return nil
}
func (ts *TarSum) Read(buf []byte) (int, error) {
if ts.gz == nil {
ts.bufTar = bytes.NewBuffer([]byte{})
ts.bufGz = bytes.NewBuffer([]byte{})
ts.tarR = tar.NewReader(ts.Reader)
ts.tarW = tar.NewWriter(ts.bufTar)
ts.gz = gzip.NewWriter(ts.bufGz)
ts.h = sha256.New()
// ts.h = verboseHash{sha256.New()}
ts.h.Reset()
ts.first = true
}
if ts.finished {
return ts.bufGz.Read(buf)
}
buf2 := make([]byte, len(buf), cap(buf))
n, err := ts.tarR.Read(buf2)
if err != nil {
if err == io.EOF {
if _, err := ts.h.Write(buf2[:n]); err != nil {
return 0, err
}
if !ts.first {
ts.sums = append(ts.sums, hex.EncodeToString(ts.h.Sum(nil)))
ts.h.Reset()
} else {
ts.first = false
}
currentHeader, err := ts.tarR.Next()
if err != nil {
if err == io.EOF {
if err := ts.gz.Close(); err != nil {
return 0, err
}
ts.finished = true
return n, nil
}
return n, err
}
if err := ts.encodeHeader(currentHeader); err != nil {
return 0, err
}
if err := ts.tarW.WriteHeader(currentHeader); err != nil {
return 0, err
}
if _, err := ts.tarW.Write(buf2[:n]); err != nil {
return 0, err
}
ts.tarW.Flush()
if _, err := io.Copy(ts.gz, ts.bufTar); err != nil {
return 0, err
}
ts.gz.Flush()
return ts.bufGz.Read(buf)
}
return n, err
}
// Filling the hash buffer
if _, err = ts.h.Write(buf2[:n]); err != nil {
return 0, err
}
// Filling the tar writter
if _, err = ts.tarW.Write(buf2[:n]); err != nil {
return 0, err
}
ts.tarW.Flush()
// Filling the gz writter
if _, err = io.Copy(ts.gz, ts.bufTar); err != nil {
return 0, err
}
ts.gz.Flush()
return ts.bufGz.Read(buf)
}
func (ts *TarSum) Sum(extra []byte) string {
sort.Strings(ts.sums)
h := sha256.New()
if extra != nil {
h.Write(extra)
}
for _, sum := range ts.sums {
Debugf("-->%s<--", sum)
h.Write([]byte(sum))
}
checksum := "tarsum+sha256:" + hex.EncodeToString(h.Sum(nil))
Debugf("checksum processed: %s", checksum)
return checksum
}

View File

@ -607,22 +607,39 @@ func NewWriteFlusher(w io.Writer) *WriteFlusher {
return &WriteFlusher{w: w, flusher: flusher} return &WriteFlusher{w: w, flusher: flusher}
} }
type JSONError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
type JSONMessage struct { type JSONMessage struct {
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Progress string `json:"progress,omitempty"` Progress string `json:"progress,omitempty"`
Error string `json:"error,omitempty"` ErrorMessage string `json:"error,omitempty"` //deprecated
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Time int64 `json:"time,omitempty"` Time int64 `json:"time,omitempty"`
Error *JSONError `json:"errorDetail,omitempty"`
} }
func (jm *JSONMessage) Display(out io.Writer) (error) { func (e *JSONError) Error() string {
return e.Message
}
func NewHTTPRequestError(msg string, res *http.Response) error {
return &JSONError{
Message: msg,
Code: res.StatusCode,
}
}
func (jm *JSONMessage) Display(out io.Writer) error {
if jm.Time != 0 { if jm.Time != 0 {
fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0)) fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0))
} }
if jm.Progress != "" { if jm.Progress != "" {
fmt.Fprintf(out, "%s %s\r", jm.Status, jm.Progress) fmt.Fprintf(out, "%s %s\r", jm.Status, jm.Progress)
} else if jm.Error != "" { } else if jm.Error != nil {
return fmt.Errorf(jm.Error) return jm.Error
} else if jm.ID != "" { } else if jm.ID != "" {
fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status) fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status)
} else { } else {
@ -631,7 +648,6 @@ func (jm *JSONMessage) Display(out io.Writer) (error) {
return nil return nil
} }
type StreamFormatter struct { type StreamFormatter struct {
json bool json bool
used bool used bool
@ -657,7 +673,11 @@ func (sf *StreamFormatter) FormatStatus(format string, a ...interface{}) []byte
func (sf *StreamFormatter) FormatError(err error) []byte { func (sf *StreamFormatter) FormatError(err error) []byte {
sf.used = true sf.used = true
if sf.json { if sf.json {
if b, err := json.Marshal(&JSONMessage{Error: err.Error()}); err == nil { jsonError, ok := err.(*JSONError)
if !ok {
jsonError = &JSONError{Message: err.Error()}
}
if b, err := json.Marshal(&JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
return b return b
} }
return []byte("{\"error\":\"format error\"}") return []byte("{\"error\":\"format error\"}")
@ -731,6 +751,22 @@ func ParseHost(host string, port int, addr string) string {
return fmt.Sprintf("tcp://%s:%d", host, port) return fmt.Sprintf("tcp://%s:%d", host, port)
} }
func GetReleaseVersion() string {
resp, err := http.Get("http://get.docker.io/latest")
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.ContentLength > 24 || resp.StatusCode != 200 {
return ""
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ""
}
return strings.TrimSpace(string(body))
}
// Get a repos name and returns the right reposName + tag // Get a repos name and returns the right reposName + tag
// The tag can be confusing because of a port in a repository name. // The tag can be confusing because of a port in a repository name.
// Ex: localhost.localdomain:5000/samalba/hipache:latest // Ex: localhost.localdomain:5000/samalba/hipache:latest

View File

@ -282,3 +282,24 @@ func TestParseHost(t *testing.T) {
t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr) t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr)
} }
} }
func TestParseRepositoryTag(t *testing.T) {
if repo, tag := ParseRepositoryTag("root"); repo != "root" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("root:tag"); repo != "root" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "tag", repo, tag)
}
if repo, tag := ParseRepositoryTag("user/repo"); repo != "user/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("user/repo:tag"); repo != "user/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "tag", repo, tag)
}
if repo, tag := ParseRepositoryTag("url:5000/repo"); repo != "url:5000/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("url:5000/repo:tag"); repo != "url:5000/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "tag", repo, tag)
}
}

View File

@ -191,7 +191,7 @@ func TestMergeConfig(t *testing.T) {
if len(configUser.Volumes) != 3 { if len(configUser.Volumes) != 3 {
t.Fatalf("Expected 3 volumes, /test1, /test2 and /test3, found %d", len(configUser.Volumes)) t.Fatalf("Expected 3 volumes, /test1, /test2 and /test3, found %d", len(configUser.Volumes))
} }
for v, _ := range configUser.Volumes { for v := range configUser.Volumes {
if v != "/test1" && v != "/test2" && v != "/test3" { if v != "/test1" && v != "/test2" && v != "/test3" {
t.Fatalf("Expected /test1 or /test2 or /test3, found %s", v) t.Fatalf("Expected /test1 or /test2 or /test3, found %s", v)
} }