зеркало из https://github.com/microsoft/docker.git
Merge branch 'master' into add_last_version-feature
This commit is contained in:
Коммит
99c27fa0dd
2
.mailmap
2
.mailmap
|
@ -23,3 +23,5 @@ Thatcher Peskens <thatcher@dotcloud.com>
|
|||
Walter Stanish <walter@pratyeka.org>
|
||||
<daniel@gasienica.ch> <dgasienica@zynga.com>
|
||||
Roberto Hashioka <roberto_hashioka@hotmail.com>
|
||||
Konstantin Pelykh <kpelykh@zettaset.com>
|
||||
David Sissitka <me@dsissitka.com>
|
||||
|
|
26
AUTHORS
26
AUTHORS
|
@ -4,12 +4,15 @@
|
|||
# For a list of active project maintainers, see the MAINTAINERS file.
|
||||
#
|
||||
Al Tobey <al@ooyala.com>
|
||||
Alex Gaynor <alex.gaynor@gmail.com>
|
||||
Alexey Shamrin <shamrin@gmail.com>
|
||||
Andrea Luzzardi <aluzzardi@gmail.com>
|
||||
Andreas Tiefenthaler <at@an-ti.eu>
|
||||
Andrew Munsell <andrew@wizardapps.net>
|
||||
Andrews Medina <andrewsmedina@gmail.com>
|
||||
Andy Rothfusz <github@metaliveblog.com>
|
||||
Andy Smith <github@anarkystic.com>
|
||||
Anthony Bishopric <git@anthonybishopric.com>
|
||||
Antony Messerli <amesserl@rackspace.com>
|
||||
Barry Allard <barry.allard@gmail.com>
|
||||
Brandon Liu <bdon@bdon.org>
|
||||
|
@ -23,17 +26,23 @@ Daniel Gasienica <daniel@gasienica.ch>
|
|||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
|
||||
Daniel Robinson <gottagetmac@gmail.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>
|
||||
Don Spaulding <donspauldingii@gmail.com>
|
||||
Dr Nic Williams <drnicwilliams@gmail.com>
|
||||
Elias Probst <mail@eliasprobst.eu>
|
||||
Eric Hanchrow <ehanchrow@ine.com>
|
||||
Evan Wies <evan@neomantra.net>
|
||||
Eric Myhre <hash@exultant.us>
|
||||
Erno Hopearuoho <erno.hopearuoho@gmail.com>
|
||||
Evan Wies <evan@neomantra.net>
|
||||
ezbercih <cem.ezberci@gmail.com>
|
||||
Fabrizio Regini <freegenie@gmail.com>
|
||||
Flavio Castelli <fcastelli@suse.com>
|
||||
Francisco Souza <f@souza.cc>
|
||||
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
|
||||
Gabriel Monroy <gabriel@opdemand.com>
|
||||
Gareth Rushgrove <gareth@morethanseven.net>
|
||||
Guillaume J. Charmes <guillaume.charmes@dotcloud.com>
|
||||
Harley Laue <losinggeneration@gmail.com>
|
||||
|
@ -41,6 +50,7 @@ Hunter Blanks <hunter@twilio.com>
|
|||
Jeff Lindsay <progrium@gmail.com>
|
||||
Jeremy Grosser <jeremy@synack.me>
|
||||
Joffrey F <joffrey@dotcloud.com>
|
||||
Johan Euphrosine <proppy@google.com>
|
||||
John Costa <john.costa@gmail.com>
|
||||
Jon Wedaman <jweede@gmail.com>
|
||||
Jonas Pfenniger <jonas@pfenniger.name>
|
||||
|
@ -48,40 +58,54 @@ Jonathan Rudenberg <jonathan@titanous.com>
|
|||
Joseph Anthony Pasquale Holsten <joseph@josephholsten.com>
|
||||
Julien Barbier <write0@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com>
|
||||
Karan Lyons <karan@karanlyons.com>
|
||||
Keli Hu <dev@keli.hu>
|
||||
Ken Cochrane <kencochrane@gmail.com>
|
||||
Kevin J. Lynagh <kevin@keminglabs.com>
|
||||
kim0 <email.ahmedkamal@googlemail.com>
|
||||
Kimbro Staken <kstaken@kstaken.com>
|
||||
Kiran Gangadharan <kiran.daredevil@gmail.com>
|
||||
Konstantin Pelykh <kpelykh@zettaset.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Marco Hennings <marco.hennings@freiheit.com>
|
||||
Marcus Farkas <toothlessgear@finitebox.com>
|
||||
Mark McGranaghan <mmcgrana@gmail.com>
|
||||
Maxim Treskin <zerthurd@gmail.com>
|
||||
meejah <meejah@meejah.ca>
|
||||
Michael Crosby <crosby.michael@gmail.com>
|
||||
Mike Gaffney <mike@uberu.com>
|
||||
Mikhail Sobolev <mss@mawhrin.net>
|
||||
Nan Monnand Deng <monnand@gmail.com>
|
||||
Nate Jones <nate@endot.org>
|
||||
Nelson Chen <crazysim@gmail.com>
|
||||
Niall O'Higgins <niallo@unworkable.org>
|
||||
Nick Stenning <nick.stenning@digital.cabinet-office.gov.uk>
|
||||
Nick Stinemates <nick@stinemates.org>
|
||||
odk- <github@odkurzacz.org>
|
||||
Paul Bowsher <pbowsher@globalpersonals.co.uk>
|
||||
Paul Hammond <paul@paulhammond.org>
|
||||
Phil Spitler <pspitler@gmail.com>
|
||||
Piotr Bogdan <ppbogdan@gmail.com>
|
||||
Renato Riccieri Santos Zannon <renato.riccieri@gmail.com>
|
||||
Rhys Hiltner <rhys@twitch.tv>
|
||||
Robert Obryk <robryk@gmail.com>
|
||||
Roberto Hashioka <roberto_hashioka@hotmail.com>
|
||||
Ryan Fowler <rwfowler@gmail.com>
|
||||
Sam Alba <sam.alba@gmail.com>
|
||||
Sam J Sharpe <sam.sharpe@digital.cabinet-office.gov.uk>
|
||||
Shawn Siefkas <shawn.siefkas@meredith.com>
|
||||
Silas Sewell <silas@sewell.org>
|
||||
Solomon Hykes <solomon@dotcloud.com>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com>
|
||||
Stefan Praszalowicz <stefan@greplin.com>
|
||||
Thatcher Peskens <thatcher@dotcloud.com>
|
||||
Thomas Bikeev <thomas.bikeev@mac.com>
|
||||
Thomas Hansen <thomas.hansen@gmail.com>
|
||||
Tianon Gravi <admwiggin@gmail.com>
|
||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||
Tobias Bieniek <Tobias.Bieniek@gmx.de>
|
||||
Tobias Schwab <tobias.schwab@dynport.de>
|
||||
Tom Hulihan <hulihan.tom159@gmail.com>
|
||||
unclejack <unclejacksons@gmail.com>
|
||||
Victor Vieux <victor.vieux@dotcloud.com>
|
||||
Vivek Agarwal <me@vivek.im>
|
||||
|
|
6
Makefile
6
Makefile
|
@ -11,7 +11,7 @@ BUILD_DIR := $(CURDIR)/.gopath
|
|||
GOPATH ?= $(BUILD_DIR)
|
||||
export GOPATH
|
||||
|
||||
GO_OPTIONS ?=
|
||||
GO_OPTIONS ?= -a -ldflags='-w -d'
|
||||
ifeq ($(VERBOSE), 1)
|
||||
GO_OPTIONS += -v
|
||||
endif
|
||||
|
@ -80,10 +80,10 @@ test:
|
|||
tar --exclude=${BUILD_SRC} -cz . | tar -xz -C ${BUILD_PATH}
|
||||
GOPATH=${CURDIR}/${BUILD_SRC} go get -d
|
||||
# Do the test
|
||||
sudo -E GOPATH=${CURDIR}/${BUILD_SRC} go test ${GO_OPTIONS}
|
||||
sudo -E GOPATH=${CURDIR}/${BUILD_SRC} CGO_ENABLED=0 go test ${GO_OPTIONS}
|
||||
|
||||
testall: all
|
||||
@(cd $(DOCKER_DIR); sudo -E go test ./... $(GO_OPTIONS))
|
||||
@(cd $(DOCKER_DIR); CGO_ENABLED=0 sudo -E go test ./... $(GO_OPTIONS))
|
||||
|
||||
fmt:
|
||||
@gofmt -s -l -w .
|
||||
|
|
334
README.md
334
README.md
|
@ -1,80 +1,129 @@
|
|||
Docker: the Linux container engine
|
||||
==================================
|
||||
|
||||
Docker is an open-source engine which automates the deployment of applications as highly portable, self-sufficient containers.
|
||||
Docker is an open-source engine which automates the deployment of
|
||||
applications as highly portable, self-sufficient containers.
|
||||
|
||||
Docker containers are both *hardware-agnostic* and *platform-agnostic*. This means that they can run anywhere, from your
|
||||
laptop to the largest EC2 compute instance and everything in between - and they don't require that you use a particular
|
||||
language, framework or packaging system. That makes them great building blocks for deploying and scaling web apps, databases
|
||||
and backend services without depending on a particular stack or provider.
|
||||
Docker containers are both *hardware-agnostic* and
|
||||
*platform-agnostic*. This means that they can run anywhere, from your
|
||||
laptop to the largest EC2 compute instance and everything in between -
|
||||
and they don't require that you use a particular language, framework
|
||||
or packaging system. That makes them great building blocks for
|
||||
deploying and scaling web apps, databases and backend services without
|
||||
depending on a particular stack or provider.
|
||||
|
||||
Docker is an open-source implementation of the deployment engine which powers [dotCloud](http://dotcloud.com), a popular Platform-as-a-Service.
|
||||
It benefits directly from the experience accumulated over several years of large-scale operation and support of hundreds of thousands
|
||||
of applications and databases.
|
||||
Docker is an open-source implementation of the deployment engine which
|
||||
powers [dotCloud](http://dotcloud.com), a popular
|
||||
Platform-as-a-Service. It benefits directly from the experience
|
||||
accumulated over several years of large-scale operation and support of
|
||||
hundreds of thousands of applications and databases.
|
||||
|
||||
![Docker L](docs/sources/concepts/images/lego_docker.jpg "Docker")
|
||||
![Docker L](docs/sources/concepts/images/dockerlogo-h.png "Docker")
|
||||
|
||||
## Better than VMs
|
||||
|
||||
A common method for distributing applications and sandbox their execution is to use virtual machines, or VMs. Typical VM formats
|
||||
are VMWare's vmdk, Oracle Virtualbox's vdi, and Amazon EC2's ami. In theory these formats should allow every developer to
|
||||
automatically package their application into a "machine" for easy distribution and deployment. In practice, that almost never
|
||||
happens, for a few reasons:
|
||||
A common method for distributing applications and sandbox their
|
||||
execution is to use virtual machines, or VMs. Typical VM formats are
|
||||
VMWare's vmdk, Oracle Virtualbox's vdi, and Amazon EC2's ami. In
|
||||
theory these formats should allow every developer to automatically
|
||||
package their application into a "machine" for easy distribution and
|
||||
deployment. In practice, that almost never happens, for a few reasons:
|
||||
|
||||
* *Size*: VMs are very large which makes them impractical to store and transfer.
|
||||
* *Performance*: running VMs consumes significant CPU and memory, which makes them impractical in many scenarios, for example local development of multi-tier applications, and
|
||||
large-scale deployment of cpu and memory-intensive applications on large numbers of machines.
|
||||
* *Portability*: competing VM environments don't play well with each other. Although conversion tools do exist, they are limited and add even more overhead.
|
||||
* *Hardware-centric*: VMs were designed with machine operators in mind, not software developers. As a result, they offer very limited tooling for what developers need most:
|
||||
building, testing and running their software. For example, VMs offer no facilities for application versioning, monitoring, configuration, logging or service discovery.
|
||||
* *Size*: VMs are very large which makes them impractical to store
|
||||
and transfer.
|
||||
* *Performance*: running VMs consumes significant CPU and memory,
|
||||
which makes them impractical in many scenarios, for example local
|
||||
development of multi-tier applications, and large-scale deployment
|
||||
of cpu and memory-intensive applications on large numbers of
|
||||
machines.
|
||||
* *Portability*: competing VM environments don't play well with each
|
||||
other. Although conversion tools do exist, they are limited and
|
||||
add even more overhead.
|
||||
* *Hardware-centric*: VMs were designed with machine operators in
|
||||
mind, not software developers. As a result, they offer very
|
||||
limited tooling for what developers need most: building, testing
|
||||
and running their software. For example, VMs offer no facilities
|
||||
for application versioning, monitoring, configuration, logging or
|
||||
service discovery.
|
||||
|
||||
By contrast, Docker relies on a different sandboxing method known as *containerization*. Unlike traditional virtualization,
|
||||
containerization takes place at the kernel level. Most modern operating system kernels now support the primitives necessary
|
||||
for containerization, including Linux with [openvz](http://openvz.org), [vserver](http://linux-vserver.org) and more recently [lxc](http://lxc.sourceforge.net),
|
||||
Solaris with [zones](http://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc) and FreeBSD with [Jails](http://www.freebsd.org/doc/handbook/jails.html).
|
||||
By contrast, Docker relies on a different sandboxing method known as
|
||||
*containerization*. Unlike traditional virtualization,
|
||||
containerization takes place at the kernel level. Most modern
|
||||
operating system kernels now support the primitives necessary for
|
||||
containerization, including Linux with [openvz](http://openvz.org),
|
||||
[vserver](http://linux-vserver.org) and more recently
|
||||
[lxc](http://lxc.sourceforge.net), Solaris with
|
||||
[zones](http://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc)
|
||||
and FreeBSD with
|
||||
[Jails](http://www.freebsd.org/doc/handbook/jails.html).
|
||||
|
||||
Docker builds on top of these low-level primitives to offer developers a portable format and runtime environment that solves
|
||||
all 4 problems. Docker containers are small (and their transfer can be optimized with layers), they have basically zero memory and cpu overhead,
|
||||
they are completely portable and are designed from the ground up with an application-centric design.
|
||||
Docker builds on top of these low-level primitives to offer developers
|
||||
a portable format and runtime environment that solves all 4
|
||||
problems. Docker containers are small (and their transfer can be
|
||||
optimized with layers), they have basically zero memory and cpu
|
||||
overhead, they are completely portable and are designed from the
|
||||
ground up with an application-centric design.
|
||||
|
||||
The best part: because docker operates at the OS level, it can still be run inside a VM!
|
||||
The best part: because ``docker`` operates at the OS level, it can
|
||||
still be run inside a VM!
|
||||
|
||||
## Plays well with others
|
||||
|
||||
Docker does not require that you buy into a particular programming language, framework, packaging system or configuration language.
|
||||
Docker does not require that you buy into a particular programming
|
||||
language, framework, packaging system or configuration language.
|
||||
|
||||
Is your application a unix process? Does it use files, tcp connections, environment variables, standard unix streams and command-line
|
||||
arguments as inputs and outputs? Then docker can run it.
|
||||
Is your application a Unix process? Does it use files, tcp
|
||||
connections, environment variables, standard Unix streams and
|
||||
command-line arguments as inputs and outputs? Then ``docker`` can run
|
||||
it.
|
||||
|
||||
Can your application's build be expressed as a sequence of such commands? Then docker can build it.
|
||||
Can your application's build be expressed as a sequence of such
|
||||
commands? Then ``docker`` can build it.
|
||||
|
||||
|
||||
## Escape dependency hell
|
||||
|
||||
A common problem for developers is the difficulty of managing all their application's dependencies in a simple and automated way.
|
||||
A common problem for developers is the difficulty of managing all
|
||||
their application's dependencies in a simple and automated way.
|
||||
|
||||
This is usually difficult for several reasons:
|
||||
|
||||
* *Cross-platform dependencies*. Modern applications often depend on a combination of system libraries and binaries, language-specific packages, framework-specific modules,
|
||||
internal components developed for another project, etc. These dependencies live in different "worlds" and require different tools - these tools typically don't work
|
||||
well with each other, requiring awkward custom integrations.
|
||||
* *Cross-platform dependencies*. Modern applications often depend on
|
||||
a combination of system libraries and binaries, language-specific
|
||||
packages, framework-specific modules, internal components
|
||||
developed for another project, etc. These dependencies live in
|
||||
different "worlds" and require different tools - these tools
|
||||
typically don't work well with each other, requiring awkward
|
||||
custom integrations.
|
||||
|
||||
* Conflicting dependencies. Different applications may depend on different versions of the same dependency. Packaging tools handle these situations with various degrees of ease -
|
||||
but they all handle them in different and incompatible ways, which again forces the developer to do extra work.
|
||||
* Conflicting dependencies. Different applications may depend on
|
||||
different versions of the same dependency. Packaging tools handle
|
||||
these situations with various degrees of ease - but they all
|
||||
handle them in different and incompatible ways, which again forces
|
||||
the developer to do extra work.
|
||||
|
||||
* Custom dependencies. A developer may need to prepare a custom version of their application's dependency. Some packaging systems can handle custom versions of a dependency,
|
||||
others can't - and all of them handle it differently.
|
||||
* Custom dependencies. A developer may need to prepare a custom
|
||||
version of their application's dependency. Some packaging systems
|
||||
can handle custom versions of a dependency, others can't - and all
|
||||
of them handle it differently.
|
||||
|
||||
|
||||
Docker solves dependency hell by giving the developer a simple way to express *all* their application's dependencies in one place,
|
||||
and streamline the process of assembling them. If this makes you think of [XKCD 927](http://xkcd.com/927/), don't worry. Docker doesn't
|
||||
*replace* your favorite packaging systems. It simply orchestrates their use in a simple and repeatable way. How does it do that? With layers.
|
||||
Docker solves dependency hell by giving the developer a simple way to
|
||||
express *all* their application's dependencies in one place, and
|
||||
streamline the process of assembling them. If this makes you think of
|
||||
[XKCD 927](http://xkcd.com/927/), don't worry. Docker doesn't
|
||||
*replace* your favorite packaging systems. It simply orchestrates
|
||||
their use in a simple and repeatable way. How does it do that? With
|
||||
layers.
|
||||
|
||||
Docker defines a build as running a sequence of unix commands, one after the other, in the same container. Build commands modify the contents of the container
|
||||
(usually by installing new files on the filesystem), the next command modifies it some more, etc. Since each build command inherits the result of the previous
|
||||
commands, the *order* in which the commands are executed expresses *dependencies*.
|
||||
Docker defines a build as running a sequence of Unix commands, one
|
||||
after the other, in the same container. Build commands modify the
|
||||
contents of the container (usually by installing new files on the
|
||||
filesystem), the next command modifies it some more, etc. Since each
|
||||
build command inherits the result of the previous commands, the
|
||||
*order* in which the commands are executed expresses *dependencies*.
|
||||
|
||||
Here's a typical docker build process:
|
||||
Here's a typical Docker build process:
|
||||
|
||||
```bash
|
||||
from ubuntu:12.10
|
||||
|
@ -87,7 +136,8 @@ run curl -L https://github.com/shykes/helloflask/archive/master.tar.gz | tar -xz
|
|||
run cd helloflask-master && pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Note that Docker doesn't care *how* dependencies are built - as long as they can be built by running a unix command in a container.
|
||||
Note that Docker doesn't care *how* dependencies are built - as long
|
||||
as they can be built by running a Unix command in a container.
|
||||
|
||||
|
||||
Install instructions
|
||||
|
@ -103,8 +153,9 @@ curl get.docker.io | sudo sh -x
|
|||
Binary installs
|
||||
----------------
|
||||
|
||||
Docker supports the following binary installation methods.
|
||||
Note that some methods are community contributions and not yet officially supported.
|
||||
Docker supports the following binary installation methods. Note that
|
||||
some methods are community contributions and not yet officially
|
||||
supported.
|
||||
|
||||
* [Ubuntu 12.04 and 12.10 (officially supported)](http://docs.docker.io/en/latest/installation/ubuntulinux/)
|
||||
* [Arch Linux](http://docs.docker.io/en/latest/installation/archlinux/)
|
||||
|
@ -115,15 +166,15 @@ Note that some methods are community contributions and not yet officially suppor
|
|||
Installing from source
|
||||
----------------------
|
||||
|
||||
1. Make sure you have a [Go language](http://golang.org/doc/install) compiler and [git](http://git-scm.com) installed.
|
||||
|
||||
1. Make sure you have a [Go language](http://golang.org/doc/install)
|
||||
compiler >= 1.1 and [git](http://git-scm.com) installed.
|
||||
2. Checkout the source code
|
||||
|
||||
```bash
|
||||
git clone http://github.com/dotcloud/docker
|
||||
```
|
||||
|
||||
3. Build the docker binary
|
||||
3. Build the ``docker`` binary
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
|
@ -134,17 +185,20 @@ Installing from source
|
|||
Usage examples
|
||||
==============
|
||||
|
||||
First run the docker daemon
|
||||
---------------------------
|
||||
First run the ``docker`` daemon
|
||||
-------------------------------
|
||||
|
||||
All the examples assume your machine is running the docker daemon. To run the docker daemon in the background, simply type:
|
||||
All the examples assume your machine is running the ``docker``
|
||||
daemon. To run the ``docker`` daemon in the background, simply type:
|
||||
|
||||
```bash
|
||||
# On a production system you want this running in an init script
|
||||
sudo docker -d &
|
||||
```
|
||||
|
||||
Now you can run docker in client mode: all commands will be forwarded to the docker daemon, so the client can run from any account.
|
||||
Now you can run ``docker`` in client mode: all commands will be
|
||||
forwarded to the ``docker`` daemon, so the client can run from any
|
||||
account.
|
||||
|
||||
```bash
|
||||
# Now you can run docker commands from any account.
|
||||
|
@ -152,7 +206,7 @@ docker help
|
|||
```
|
||||
|
||||
|
||||
Throwaway shell in a base ubuntu image
|
||||
Throwaway shell in a base Ubuntu image
|
||||
--------------------------------------
|
||||
|
||||
```bash
|
||||
|
@ -202,7 +256,8 @@ docker commit -m "Installed curl" $CONTAINER $USER/betterbase
|
|||
docker push $USER/betterbase
|
||||
```
|
||||
|
||||
A list of publicly available images is [available here](https://github.com/dotcloud/docker/wiki/Public-docker-images).
|
||||
A list of publicly available images is [available
|
||||
here](https://github.com/dotcloud/docker/wiki/Public-docker-images).
|
||||
|
||||
Expose a service on a TCP port
|
||||
------------------------------
|
||||
|
@ -229,32 +284,40 @@ Under the hood
|
|||
|
||||
Under the hood, Docker is built on the following components:
|
||||
|
||||
|
||||
* The [cgroup](http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-24-c) and [namespacing](http://blog.dotcloud.com/under-the-hood-linux-kernels-on-dotcloud-part) capabilities of the Linux kernel;
|
||||
|
||||
* [AUFS](http://aufs.sourceforge.net/aufs.html), a powerful union filesystem with copy-on-write capabilities;
|
||||
|
||||
* The
|
||||
[cgroup](http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-24-c)
|
||||
and
|
||||
[namespacing](http://blog.dotcloud.com/under-the-hood-linux-kernels-on-dotcloud-part)
|
||||
capabilities of the Linux kernel;
|
||||
* [AUFS](http://aufs.sourceforge.net/aufs.html), a powerful union
|
||||
filesystem with copy-on-write capabilities;
|
||||
* The [Go](http://golang.org) programming language;
|
||||
|
||||
* [lxc](http://lxc.sourceforge.net/), a set of convenience scripts to simplify the creation of linux containers.
|
||||
* [lxc](http://lxc.sourceforge.net/), a set of convenience scripts to
|
||||
simplify the creation of Linux containers.
|
||||
|
||||
|
||||
|
||||
Contributing to Docker
|
||||
======================
|
||||
|
||||
Want to hack on Docker? Awesome! There are instructions to get you started on the website: http://docs.docker.io/en/latest/contributing/contributing/
|
||||
Want to hack on Docker? Awesome! There are instructions to get you
|
||||
started on the website:
|
||||
http://docs.docker.io/en/latest/contributing/contributing/
|
||||
|
||||
They are probably not perfect, please let us know if anything feels wrong or incomplete.
|
||||
They are probably not perfect, please let us know if anything feels
|
||||
wrong or incomplete.
|
||||
|
||||
|
||||
Note
|
||||
----
|
||||
|
||||
We also keep the documentation in this repository. The website documentation is generated using sphinx using these sources.
|
||||
Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/tree/master/docs/README.md
|
||||
We also keep the documentation in this repository. The website
|
||||
documentation is generated using Sphinx using these sources. Please
|
||||
find it under docs/sources/ and read more about it
|
||||
https://github.com/dotcloud/docker/tree/master/docs/README.md
|
||||
|
||||
Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome.
|
||||
Please feel free to fix / update the documentation and send us pull
|
||||
requests. More tutorials are also welcome.
|
||||
|
||||
|
||||
Setting up a dev environment
|
||||
|
@ -289,93 +352,104 @@ Run the `go install` command (above) to recompile docker.
|
|||
What is a Standard Container?
|
||||
=============================
|
||||
|
||||
Docker defines a unit of software delivery called a Standard Container. The goal of a Standard Container is to encapsulate a software component and all its dependencies in
|
||||
a format that is self-describing and portable, so that any compliant runtime can run it without extra dependencies, regardless of the underlying machine and the contents of the container.
|
||||
Docker defines a unit of software delivery called a Standard
|
||||
Container. The goal of a Standard Container is to encapsulate a
|
||||
software component and all its dependencies in a format that is
|
||||
self-describing and portable, so that any compliant runtime can run it
|
||||
without extra dependencies, regardless of the underlying machine and
|
||||
the contents of the container.
|
||||
|
||||
The spec for Standard Containers is currently a work in progress, but it is very straightforward. It mostly defines 1) an image format, 2) a set of standard operations, and 3) an execution environment.
|
||||
The spec for Standard Containers is currently a work in progress, but
|
||||
it is very straightforward. It mostly defines 1) an image format, 2) a
|
||||
set of standard operations, and 3) an execution environment.
|
||||
|
||||
A great analogy for this is the shipping container. Just like how Standard Containers are a fundamental unit of software delivery, shipping containers (http://bricks.argz.com/ins/7823-1/12) are a fundamental unit of physical delivery.
|
||||
A great analogy for this is the shipping container. Just like how
|
||||
Standard Containers are a fundamental unit of software delivery,
|
||||
shipping containers are a fundamental unit of physical delivery.
|
||||
|
||||
### 1. STANDARD OPERATIONS
|
||||
|
||||
Just like shipping containers, Standard Containers define a set of STANDARD OPERATIONS. Shipping containers can be lifted, stacked, locked, loaded, unloaded and labelled. Similarly, standard containers can be started, stopped, copied, snapshotted, downloaded, uploaded and tagged.
|
||||
Just like shipping containers, Standard Containers define a set of
|
||||
STANDARD OPERATIONS. Shipping containers can be lifted, stacked,
|
||||
locked, loaded, unloaded and labelled. Similarly, Standard Containers
|
||||
can be started, stopped, copied, snapshotted, downloaded, uploaded and
|
||||
tagged.
|
||||
|
||||
|
||||
### 2. CONTENT-AGNOSTIC
|
||||
|
||||
Just like shipping containers, Standard Containers are CONTENT-AGNOSTIC: all standard operations have the same effect regardless of the contents. A shipping container will be stacked in exactly the same way whether it contains Vietnamese powder coffee or spare Maserati parts. Similarly, Standard Containers are started or uploaded in the same way whether they contain a postgres database, a php application with its dependencies and application server, or Java build artifacts.
|
||||
Just like shipping containers, Standard Containers are
|
||||
CONTENT-AGNOSTIC: all standard operations have the same effect
|
||||
regardless of the contents. A shipping container will be stacked in
|
||||
exactly the same way whether it contains Vietnamese powder coffee or
|
||||
spare Maserati parts. Similarly, Standard Containers are started or
|
||||
uploaded in the same way whether they contain a postgres database, a
|
||||
php application with its dependencies and application server, or Java
|
||||
build artifacts.
|
||||
|
||||
|
||||
### 3. INFRASTRUCTURE-AGNOSTIC
|
||||
|
||||
Both types of containers are INFRASTRUCTURE-AGNOSTIC: they can be transported to thousands of facilities around the world, and manipulated by a wide variety of equipment. A shipping container can be packed in a factory in Ukraine, transported by truck to the nearest routing center, stacked onto a train, loaded into a German boat by an Australian-built crane, stored in a warehouse at a US facility, etc. Similarly, a standard container can be bundled on my laptop, uploaded to S3, downloaded, run and snapshotted by a build server at Equinix in Virginia, uploaded to 10 staging servers in a home-made Openstack cluster, then sent to 30 production instances across 3 EC2 regions.
|
||||
Both types of containers are INFRASTRUCTURE-AGNOSTIC: they can be
|
||||
transported to thousands of facilities around the world, and
|
||||
manipulated by a wide variety of equipment. A shipping container can
|
||||
be packed in a factory in Ukraine, transported by truck to the nearest
|
||||
routing center, stacked onto a train, loaded into a German boat by an
|
||||
Australian-built crane, stored in a warehouse at a US facility,
|
||||
etc. Similarly, a standard container can be bundled on my laptop,
|
||||
uploaded to S3, downloaded, run and snapshotted by a build server at
|
||||
Equinix in Virginia, uploaded to 10 staging servers in a home-made
|
||||
Openstack cluster, then sent to 30 production instances across 3 EC2
|
||||
regions.
|
||||
|
||||
|
||||
### 4. DESIGNED FOR AUTOMATION
|
||||
|
||||
Because they offer the same standard operations regardless of content and infrastructure, Standard Containers, just like their physical counterpart, are extremely well-suited for automation. In fact, you could say automation is their secret weapon.
|
||||
Because they offer the same standard operations regardless of content
|
||||
and infrastructure, Standard Containers, just like their physical
|
||||
counterparts, are extremely well-suited for automation. In fact, you
|
||||
could say automation is their secret weapon.
|
||||
|
||||
Many things that once required time-consuming and error-prone human effort can now be programmed. Before shipping containers, a bag of powder coffee was hauled, dragged, dropped, rolled and stacked by 10 different people in 10 different locations by the time it reached its destination. 1 out of 50 disappeared. 1 out of 20 was damaged. The process was slow, inefficient and cost a fortune - and was entirely different depending on the facility and the type of goods.
|
||||
Many things that once required time-consuming and error-prone human
|
||||
effort can now be programmed. Before shipping containers, a bag of
|
||||
powder coffee was hauled, dragged, dropped, rolled and stacked by 10
|
||||
different people in 10 different locations by the time it reached its
|
||||
destination. 1 out of 50 disappeared. 1 out of 20 was damaged. The
|
||||
process was slow, inefficient and cost a fortune - and was entirely
|
||||
different depending on the facility and the type of goods.
|
||||
|
||||
Similarly, before Standard Containers, by the time a software component ran in production, it had been individually built, configured, bundled, documented, patched, vendored, templated, tweaked and instrumented by 10 different people on 10 different computers. Builds failed, libraries conflicted, mirrors crashed, post-it notes were lost, logs were misplaced, cluster updates were half-broken. The process was slow, inefficient and cost a fortune - and was entirely different depending on the language and infrastructure provider.
|
||||
Similarly, before Standard Containers, by the time a software
|
||||
component ran in production, it had been individually built,
|
||||
configured, bundled, documented, patched, vendored, templated, tweaked
|
||||
and instrumented by 10 different people on 10 different
|
||||
computers. Builds failed, libraries conflicted, mirrors crashed,
|
||||
post-it notes were lost, logs were misplaced, cluster updates were
|
||||
half-broken. The process was slow, inefficient and cost a fortune -
|
||||
and was entirely different depending on the language and
|
||||
infrastructure provider.
|
||||
|
||||
|
||||
### 5. INDUSTRIAL-GRADE DELIVERY
|
||||
|
||||
There are 17 million shipping containers in existence, packed with every physical good imaginable. Every single one of them can be loaded onto the same boats, by the same cranes, in the same facilities, and sent anywhere in the World with incredible efficiency. It is embarrassing to think that a 30 ton shipment of coffee can safely travel half-way across the World in *less time* than it takes a software team to deliver its code from one datacenter to another sitting 10 miles away.
|
||||
There are 17 million shipping containers in existence, packed with
|
||||
every physical good imaginable. Every single one of them can be loaded
|
||||
onto the same boats, by the same cranes, in the same facilities, and
|
||||
sent anywhere in the World with incredible efficiency. It is
|
||||
embarrassing to think that a 30 ton shipment of coffee can safely
|
||||
travel half-way across the World in *less time* than it takes a
|
||||
software team to deliver its code from one datacenter to another
|
||||
sitting 10 miles away.
|
||||
|
||||
With Standard Containers we can put an end to that embarrassment, by making INDUSTRIAL-GRADE DELIVERY of software a reality.
|
||||
With Standard Containers we can put an end to that embarrassment, by
|
||||
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
|
||||
|
||||
Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
|
||||
legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
|
||||
and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
|
||||
Transfers of Docker shall be in accordance with applicable export
|
||||
controls of any country and all other applicable legal requirements.
|
||||
Docker shall not be distributed or downloaded to or in Cuba, Iran,
|
||||
North Korea, Sudan or Syria and shall not be distributed or downloaded
|
||||
to any person on the Denied Persons List administered by the U.S.
|
||||
Department of Commerce.
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ Vagrant::Config.run do |config|
|
|||
pkg_cmd = "apt-get update -qq; apt-get install -q -y python-software-properties; " \
|
||||
"add-apt-repository -y ppa:dotcloud/lxc-docker; apt-get update -qq; " \
|
||||
"apt-get install -q -y lxc-docker; "
|
||||
# Listen on all interfaces so that the daemon is accessible from the host
|
||||
pkg_cmd << "sed -i -E 's| /usr/bin/docker -d| /usr/bin/docker -d -H 0.0.0.0|' /etc/init/docker.conf;"
|
||||
# Add X.org Ubuntu backported 3.8 kernel
|
||||
pkg_cmd << "add-apt-repository -y ppa:ubuntu-x-swat/r-lts-backport; " \
|
||||
"apt-get update -qq; apt-get install -q -y linux-image-3.8.0-19-generic; "
|
||||
|
|
130
api.go
130
api.go
|
@ -17,7 +17,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const APIVERSION = 1.3
|
||||
const APIVERSION = 1.4
|
||||
const DEFAULTHTTPHOST string = "127.0.0.1"
|
||||
const DEFAULTHTTPPORT int = 4243
|
||||
|
||||
|
@ -81,54 +81,15 @@ func getBoolParam(value string) (bool, error) {
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if version > 1.1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
authConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
authConfig = &auth.AuthConfig{}
|
||||
}
|
||||
b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
authConfig := &auth.AuthConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := ""
|
||||
if version > 1.1 {
|
||||
status, err = auth.Login(authConfig, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if authConfig.Username == localAuthConfig.Username {
|
||||
authConfig.Password = localAuthConfig.Password
|
||||
}
|
||||
|
||||
newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root)
|
||||
status, err = auth.Login(newAuthConfig, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := auth.Login(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "" {
|
||||
b, err := json.Marshal(&APIAuth{Status: status})
|
||||
|
@ -217,6 +178,64 @@ func getInfo(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
|
|||
return nil
|
||||
}
|
||||
|
||||
func getEvents(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
sendEvent := func(wf *utils.WriteFlusher, event *utils.JSONMessage) error {
|
||||
b, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JSON error")
|
||||
}
|
||||
_, err = wf.Write(b)
|
||||
if err != nil {
|
||||
// On error, evict the listener
|
||||
utils.Debugf("%s", err)
|
||||
srv.Lock()
|
||||
delete(srv.listeners, r.RemoteAddr)
|
||||
srv.Unlock()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
listener := make(chan utils.JSONMessage)
|
||||
srv.Lock()
|
||||
srv.listeners[r.RemoteAddr] = listener
|
||||
srv.Unlock()
|
||||
since, err := strconv.ParseInt(r.Form.Get("since"), 10, 0)
|
||||
if err != nil {
|
||||
since = 0
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
wf := utils.NewWriteFlusher(w)
|
||||
if since != 0 {
|
||||
// If since, send previous events that happened after the timestamp
|
||||
for _, event := range srv.events {
|
||||
if event.Time >= since {
|
||||
err := sendEvent(wf, &event)
|
||||
if err != nil && err.Error() == "JSON error" {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
event := <-listener
|
||||
err := sendEvent(wf, &event)
|
||||
if err != nil && err.Error() == "JSON error" {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImagesHistory(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if vars == nil {
|
||||
return fmt.Errorf("Missing parameter")
|
||||
|
@ -252,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 {
|
||||
if version < 1.4 {
|
||||
return fmt.Errorf("top was improved a lot since 1.3, Please upgrade your docker client.")
|
||||
}
|
||||
if vars == nil {
|
||||
return fmt.Errorf("Missing parameter")
|
||||
}
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -429,16 +455,8 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
|
|||
|
||||
func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
authConfig := &auth.AuthConfig{}
|
||||
if version > 1.1 {
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil && err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
authConfig = localAuthConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
|
@ -854,9 +872,9 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
|
|||
|
||||
m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{
|
||||
"GET": {
|
||||
"/auth": getAuth,
|
||||
"/version": getVersion,
|
||||
"/events": getEvents,
|
||||
"/info": getInfo,
|
||||
"/version": getVersion,
|
||||
"/images/json": getImagesJSON,
|
||||
"/images/viz": getImagesViz,
|
||||
"/images/search": getImagesSearch,
|
||||
|
|
|
@ -17,20 +17,21 @@ type APIImages struct {
|
|||
}
|
||||
|
||||
type APIInfo struct {
|
||||
Debug bool
|
||||
Containers int
|
||||
Images int
|
||||
NFd int `json:",omitempty"`
|
||||
NGoroutines int `json:",omitempty"`
|
||||
MemoryLimit bool `json:",omitempty"`
|
||||
SwapLimit bool `json:",omitempty"`
|
||||
Debug bool
|
||||
Containers int
|
||||
Images int
|
||||
NFd int `json:",omitempty"`
|
||||
NGoroutines int `json:",omitempty"`
|
||||
MemoryLimit bool `json:",omitempty"`
|
||||
SwapLimit bool `json:",omitempty"`
|
||||
LXCVersion string `json:",omitempty"`
|
||||
NEventsListener int `json:",omitempty"`
|
||||
KernelVersion string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type APITop struct {
|
||||
PID string
|
||||
Tty string
|
||||
Time string
|
||||
Cmd string
|
||||
Titles []string
|
||||
Processes [][]string
|
||||
}
|
||||
|
||||
type APIRmi struct {
|
||||
|
|
73
api_test.go
73
api_test.go
|
@ -89,6 +89,44 @@ func TestGetInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetEvents(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
events: make([]utils.JSONMessage, 0, 64),
|
||||
listeners: make(map[string]chan utils.JSONMessage),
|
||||
}
|
||||
|
||||
srv.LogEvent("fakeaction", "fakeid")
|
||||
srv.LogEvent("fakeaction2", "fakeid")
|
||||
|
||||
req, err := http.NewRequest("GET", "/events?since=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
setTimeout(t, "", 500*time.Millisecond, func() {
|
||||
if err := getEvents(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
for i := 0; i < 2; i++ {
|
||||
var jm utils.JSONMessage
|
||||
if err := dec.Decode(&jm); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if jm != srv.events[i] {
|
||||
t.Fatalf("Event received it different than expected")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetImagesJSON(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
@ -444,24 +482,33 @@ func TestGetContainersTop(t *testing.T) {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(procs) != 2 {
|
||||
t.Fatalf("Expected 2 processes, found %d.", len(procs))
|
||||
if len(procs.Titles) != 11 {
|
||||
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" {
|
||||
t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[0].Cmd)
|
||||
if len(procs.Processes) != 2 {
|
||||
t.Fatalf("Expected 2 processes, found %d.", len(procs.Processes))
|
||||
}
|
||||
|
||||
if procs[1].Cmd != "sh" && procs[1].Cmd != "busybox" {
|
||||
t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[1].Cmd)
|
||||
if procs.Processes[0][10] != "/bin/sh" && procs.Processes[0][10] != "sleep" {
|
||||
t.Fatalf("Expected `sleep` or `/bin/sh`, found %s.", procs.Processes[0][10])
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -857,6 +904,12 @@ func TestPostContainersAttach(t *testing.T) {
|
|||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
// Try to avoid the timeoout in destroy. Best effort, don't check error
|
||||
defer func() {
|
||||
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
|
||||
container.Kill()
|
||||
}()
|
||||
|
||||
// Attach to it
|
||||
c1 := make(chan struct{})
|
||||
go func() {
|
||||
|
@ -896,7 +949,7 @@ func TestPostContainersAttach(t *testing.T) {
|
|||
}
|
||||
|
||||
// Wait for attach to finish, the client disconnected, therefore, Attach finished his job
|
||||
setTimeout(t, "Waiting for CmdAttach timed out", 2*time.Second, func() {
|
||||
setTimeout(t, "Waiting for CmdAttach timed out", 10*time.Second, func() {
|
||||
<-c1
|
||||
})
|
||||
|
||||
|
|
112
auth/auth.go
112
auth/auth.go
|
@ -18,26 +18,22 @@ const CONFIGFILE = ".dockercfg"
|
|||
// Only used for user auth + account creation
|
||||
const INDEXSERVER = "https://index.docker.io/v1/"
|
||||
|
||||
//const INDEXSERVER = "http://indexstaging-docker.dotcloud.com/"
|
||||
//const INDEXSERVER = "https://indexstaging-docker.dotcloud.com/v1/"
|
||||
|
||||
var (
|
||||
ErrConfigFileMissing = errors.New("The Auth config file is missing")
|
||||
)
|
||||
|
||||
type AuthConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Auth string `json:"auth"`
|
||||
Email string `json:"email"`
|
||||
rootPath string
|
||||
}
|
||||
|
||||
func NewAuthConfig(username, password, email, rootPath string) *AuthConfig {
|
||||
return &AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Email: email,
|
||||
rootPath: rootPath,
|
||||
}
|
||||
type ConfigFile struct {
|
||||
Configs map[string]AuthConfig `json:"configs,omitempty"`
|
||||
rootPath string
|
||||
}
|
||||
|
||||
func IndexServerAddress() string {
|
||||
|
@ -54,61 +50,89 @@ func encodeAuth(authConfig *AuthConfig) string {
|
|||
}
|
||||
|
||||
// decode the auth string
|
||||
func decodeAuth(authStr string) (*AuthConfig, error) {
|
||||
func decodeAuth(authStr string) (string, string, error) {
|
||||
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
||||
decoded := make([]byte, decLen)
|
||||
authByte := []byte(authStr)
|
||||
n, err := base64.StdEncoding.Decode(decoded, authByte)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", "", err
|
||||
}
|
||||
if n > decLen {
|
||||
return nil, fmt.Errorf("Something went wrong decoding auth config")
|
||||
return "", "", fmt.Errorf("Something went wrong decoding auth config")
|
||||
}
|
||||
arr := strings.Split(string(decoded), ":")
|
||||
if len(arr) != 2 {
|
||||
return nil, fmt.Errorf("Invalid auth configuration file")
|
||||
return "", "", fmt.Errorf("Invalid auth configuration file")
|
||||
}
|
||||
password := strings.Trim(arr[1], "\x00")
|
||||
return &AuthConfig{Username: arr[0], Password: password}, nil
|
||||
return arr[0], password, nil
|
||||
}
|
||||
|
||||
// load up the auth config information and return values
|
||||
// FIXME: use the internal golang config parser
|
||||
func LoadConfig(rootPath string) (*AuthConfig, error) {
|
||||
func LoadConfig(rootPath string) (*ConfigFile, error) {
|
||||
configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
|
||||
confFile := path.Join(rootPath, CONFIGFILE)
|
||||
if _, err := os.Stat(confFile); err != nil {
|
||||
return &AuthConfig{rootPath: rootPath}, ErrConfigFileMissing
|
||||
return &configFile, ErrConfigFileMissing
|
||||
}
|
||||
b, err := ioutil.ReadFile(confFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr := strings.Split(string(b), "\n")
|
||||
if len(arr) < 2 {
|
||||
return nil, fmt.Errorf("The Auth config file is empty")
|
||||
|
||||
if err := json.Unmarshal(b, &configFile.Configs); err != nil {
|
||||
arr := strings.Split(string(b), "\n")
|
||||
if len(arr) < 2 {
|
||||
return nil, fmt.Errorf("The Auth config file is empty")
|
||||
}
|
||||
authConfig := AuthConfig{}
|
||||
origAuth := strings.Split(arr[0], " = ")
|
||||
authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origEmail := strings.Split(arr[1], " = ")
|
||||
authConfig.Email = origEmail[1]
|
||||
configFile.Configs[IndexServerAddress()] = authConfig
|
||||
} else {
|
||||
for k, authConfig := range configFile.Configs {
|
||||
authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authConfig.Auth = ""
|
||||
configFile.Configs[k] = authConfig
|
||||
}
|
||||
}
|
||||
origAuth := strings.Split(arr[0], " = ")
|
||||
origEmail := strings.Split(arr[1], " = ")
|
||||
authConfig, err := decodeAuth(origAuth[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authConfig.Email = origEmail[1]
|
||||
authConfig.rootPath = rootPath
|
||||
return authConfig, nil
|
||||
return &configFile, nil
|
||||
}
|
||||
|
||||
// save the auth config
|
||||
func SaveConfig(authConfig *AuthConfig) error {
|
||||
confFile := path.Join(authConfig.rootPath, CONFIGFILE)
|
||||
if len(authConfig.Email) == 0 {
|
||||
func SaveConfig(configFile *ConfigFile) error {
|
||||
confFile := path.Join(configFile.rootPath, CONFIGFILE)
|
||||
if len(configFile.Configs) == 0 {
|
||||
os.Remove(confFile)
|
||||
return nil
|
||||
}
|
||||
lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n"
|
||||
b := []byte(lines)
|
||||
err := ioutil.WriteFile(confFile, b, 0600)
|
||||
|
||||
configs := make(map[string]AuthConfig, len(configFile.Configs))
|
||||
for k, authConfig := range configFile.Configs {
|
||||
authCopy := authConfig
|
||||
|
||||
authCopy.Auth = encodeAuth(&authCopy)
|
||||
authCopy.Username = ""
|
||||
authCopy.Password = ""
|
||||
|
||||
configs[k] = authCopy
|
||||
}
|
||||
|
||||
b, err := json.Marshal(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(confFile, b, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -116,8 +140,7 @@ func SaveConfig(authConfig *AuthConfig) error {
|
|||
}
|
||||
|
||||
// try to register/login to the registry server
|
||||
func Login(authConfig *AuthConfig, store bool) (string, error) {
|
||||
storeConfig := false
|
||||
func Login(authConfig *AuthConfig) (string, error) {
|
||||
client := &http.Client{}
|
||||
reqStatusCode := 0
|
||||
var status string
|
||||
|
@ -143,7 +166,6 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
|
|||
if reqStatusCode == 201 {
|
||||
status = "Account created. Please use the confirmation link we sent" +
|
||||
" to your e-mail to activate it."
|
||||
storeConfig = true
|
||||
} else if reqStatusCode == 403 {
|
||||
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
|
||||
"Please check your e-mail for a confirmation link.")
|
||||
|
@ -162,14 +184,7 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
|
|||
}
|
||||
if resp.StatusCode == 200 {
|
||||
status = "Login Succeeded"
|
||||
storeConfig = true
|
||||
} else if resp.StatusCode == 401 {
|
||||
if store {
|
||||
authConfig.Email = ""
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Wrong login/password, please try again")
|
||||
} else {
|
||||
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
|
||||
|
@ -181,10 +196,5 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
|
|||
} else {
|
||||
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
|
||||
}
|
||||
if storeConfig && store {
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package auth
|
|||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -11,7 +12,9 @@ import (
|
|||
func TestEncodeAuth(t *testing.T) {
|
||||
newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
|
||||
authStr := encodeAuth(newAuthConfig)
|
||||
decAuthConfig, err := decodeAuth(authStr)
|
||||
decAuthConfig := &AuthConfig{}
|
||||
var err error
|
||||
decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -29,8 +32,8 @@ func TestEncodeAuth(t *testing.T) {
|
|||
func TestLogin(t *testing.T) {
|
||||
os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
|
||||
defer os.Setenv("DOCKER_INDEX_URL", "")
|
||||
authConfig := NewAuthConfig("unittester", "surlautrerivejetattendrai", "noise+unittester@dotcloud.com", "/tmp")
|
||||
status, err := Login(authConfig, false)
|
||||
authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
|
||||
status, err := Login(authConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -49,8 +52,8 @@ func TestCreateAccount(t *testing.T) {
|
|||
}
|
||||
token := hex.EncodeToString(tokenBuffer)[:12]
|
||||
username := "ut" + token
|
||||
authConfig := NewAuthConfig(username, "test42", "docker-ut+"+token+"@example.com", "/tmp")
|
||||
status, err := Login(authConfig, false)
|
||||
authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+" + token + "@example.com"}
|
||||
status, err := Login(authConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -60,7 +63,7 @@ func TestCreateAccount(t *testing.T) {
|
|||
t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
|
||||
}
|
||||
|
||||
status, err = Login(authConfig, false)
|
||||
status, err = Login(authConfig)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but found nil instead")
|
||||
}
|
||||
|
@ -71,3 +74,39 @@ func TestCreateAccount(t *testing.T) {
|
|||
t.Fatalf("Expected message \"%s\" but found \"%s\" instead", expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameAuthDataPostSave(t *testing.T) {
|
||||
root, err := ioutil.TempDir("", "docker-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configFile := &ConfigFile{
|
||||
rootPath: root,
|
||||
Configs: make(map[string]AuthConfig, 1),
|
||||
}
|
||||
|
||||
configFile.Configs["testIndex"] = AuthConfig{
|
||||
Username: "docker-user",
|
||||
Password: "docker-pass",
|
||||
Email: "docker@docker.io",
|
||||
}
|
||||
|
||||
err = SaveConfig(configFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authConfig := configFile.Configs["testIndex"]
|
||||
if authConfig.Username != "docker-user" {
|
||||
t.Fail()
|
||||
}
|
||||
if authConfig.Password != "docker-pass" {
|
||||
t.Fail()
|
||||
}
|
||||
if authConfig.Email != "docker@docker.io" {
|
||||
t.Fail()
|
||||
}
|
||||
if authConfig.Auth != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
// FIXME: freeze the container before copying it to avoid data corruption?
|
||||
// FIXME: this shouldn't be in commands.
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rwTar, err := container.ExportRw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
92
buildfile.go
92
buildfile.go
|
@ -7,9 +7,11 @@ import (
|
|||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -66,6 +68,9 @@ func (b *buildFile) CmdFrom(name string) error {
|
|||
}
|
||||
b.image = image.ID
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -111,6 +116,40 @@ func (b *buildFile) CmdRun(args string) error {
|
|||
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 {
|
||||
tmp := strings.SplitN(args, " ", 2)
|
||||
if len(tmp) != 2 {
|
||||
|
@ -119,14 +158,19 @@ func (b *buildFile) CmdEnv(args string) error {
|
|||
key := strings.Trim(tmp[0], " \t")
|
||||
value := strings.Trim(tmp[1], " \t")
|
||||
|
||||
for i, elem := range b.config.Env {
|
||||
if strings.HasPrefix(elem, key+"=") {
|
||||
b.config.Env[i] = key + "=" + value
|
||||
return nil
|
||||
}
|
||||
envKey := b.FindEnvKey(key)
|
||||
replacedValue, err := b.ReplaceEnvMatches(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.Env = append(b.config.Env, key+"="+value)
|
||||
return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, value))
|
||||
replacedVar := fmt.Sprintf("%s=%s", key, replacedValue)
|
||||
|
||||
if envKey >= 0 {
|
||||
b.config.Env[envKey] = replacedVar
|
||||
return nil
|
||||
}
|
||||
b.config.Env = append(b.config.Env, replacedVar)
|
||||
return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s", replacedVar))
|
||||
}
|
||||
|
||||
func (b *buildFile) CmdCmd(args string) error {
|
||||
|
@ -201,6 +245,24 @@ func (b *buildFile) addRemote(container *Container, orig, dest string) error {
|
|||
}
|
||||
defer file.Body.Close()
|
||||
|
||||
// If the destination is a directory, figure out the filename.
|
||||
if strings.HasSuffix(dest, "/") {
|
||||
u, err := url.Parse(orig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := u.Path
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
parts := strings.Split(path, "/")
|
||||
filename := parts[len(parts)-1]
|
||||
if filename == "" {
|
||||
return fmt.Errorf("cannot determine filename from url: %s", u)
|
||||
}
|
||||
dest = dest + filename
|
||||
}
|
||||
|
||||
return container.Inject(file.Body, dest)
|
||||
}
|
||||
|
||||
|
@ -208,7 +270,7 @@ func (b *buildFile) addContext(container *Container, orig, dest string) error {
|
|||
origPath := path.Join(b.context, orig)
|
||||
destPath := path.Join(container.RootfsPath(), dest)
|
||||
// Preserve the trailing '/'
|
||||
if dest[len(dest)-1] == '/' {
|
||||
if strings.HasSuffix(dest, "/") {
|
||||
destPath = destPath + "/"
|
||||
}
|
||||
fi, err := os.Stat(origPath)
|
||||
|
@ -223,7 +285,7 @@ func (b *buildFile) addContext(container *Container, orig, dest string) error {
|
|||
} else if err := UntarPath(origPath, destPath); err != nil {
|
||||
utils.Debugf("Couldn't untar %s to %s: %s", origPath, destPath, err)
|
||||
// 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
|
||||
}
|
||||
if err := CopyWithTar(origPath, destPath); err != nil {
|
||||
|
@ -241,8 +303,16 @@ func (b *buildFile) CmdAdd(args string) error {
|
|||
if len(tmp) != 2 {
|
||||
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
|
||||
b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
|
||||
|
|
|
@ -3,13 +3,17 @@ package docker
|
|||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mkTestContext generates a build context from the contents of the provided dockerfile.
|
||||
// This context is suitable for use as an argument to BuildFile.Build()
|
||||
func mkTestContext(dockerfile string, files [][2]string, t *testing.T) Archive {
|
||||
context, err := mkBuildContext(fmt.Sprintf(dockerfile, unitTestImageID), files)
|
||||
context, err := mkBuildContext(dockerfile, files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -22,6 +26,8 @@ type testContextTemplate struct {
|
|||
dockerfile string
|
||||
// Additional files in the context, eg [][2]string{"./passwd", "gordon"}
|
||||
files [][2]string
|
||||
// Additional remote files to host on a local HTTP server.
|
||||
remoteFiles [][2]string
|
||||
}
|
||||
|
||||
// A table of all the contexts to build and test.
|
||||
|
@ -29,27 +35,31 @@ type testContextTemplate struct {
|
|||
var testContexts = []testContextTemplate{
|
||||
{
|
||||
`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
run sh -c 'echo root:testpass > /tmp/passwd'
|
||||
run mkdir -p /var/run/sshd
|
||||
run [ "$(cat /tmp/passwd)" = "root:testpass" ]
|
||||
run [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]
|
||||
`,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
add foo /usr/lib/bla/bar
|
||||
run [ "$(cat /usr/lib/bla/bar)" = 'hello world!' ]
|
||||
run [ "$(cat /usr/lib/bla/bar)" = 'hello' ]
|
||||
add http://{SERVERADDR}/baz /usr/lib/baz/quux
|
||||
run [ "$(cat /usr/lib/baz/quux)" = 'world!' ]
|
||||
`,
|
||||
[][2]string{{"foo", "hello world!"}},
|
||||
[][2]string{{"foo", "hello"}},
|
||||
[][2]string{{"/baz", "world!"}},
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
add f /
|
||||
run [ "$(cat /f)" = "hello" ]
|
||||
add f /abc
|
||||
|
@ -71,38 +81,118 @@ run [ "$(cat /somewheeeere/over/the/rainbooow/ga)" = "bu" ]
|
|||
{"f", "hello"},
|
||||
{"d/ga", "bu"},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
add http://{SERVERADDR}/x /a/b/c
|
||||
run [ "$(cat /a/b/c)" = "hello" ]
|
||||
add http://{SERVERADDR}/x?foo=bar /
|
||||
run [ "$(cat /x)" = "hello" ]
|
||||
add http://{SERVERADDR}/x /d/
|
||||
run [ "$(cat /d/x)" = "hello" ]
|
||||
add http://{SERVERADDR} /e
|
||||
run [ "$(cat /e)" = "blah" ]
|
||||
`,
|
||||
nil,
|
||||
[][2]string{{"/x", "hello"}, {"/", "blah"}},
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from {IMAGE}
|
||||
env FOO BAR
|
||||
run [ "$FOO" = "BAR" ]
|
||||
`,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
ENTRYPOINT /bin/echo
|
||||
CMD Hello world
|
||||
`,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
ENTRYPOINT /bin/echo
|
||||
CMD Hello world
|
||||
`,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from {IMAGE}
|
||||
VOLUME /test
|
||||
CMD Hello world
|
||||
`,
|
||||
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
|
||||
|
||||
func constructDockerfile(template string, ip net.IP, port string) string {
|
||||
serverAddr := fmt.Sprintf("%s:%s", ip, port)
|
||||
replacer := strings.NewReplacer("{IMAGE}", unitTestImageID, "{SERVERADDR}", serverAddr)
|
||||
return replacer.Replace(template)
|
||||
}
|
||||
|
||||
func mkTestingFileServer(files [][2]string) (*httptest.Server, error) {
|
||||
mux := http.NewServeMux()
|
||||
for _, file := range files {
|
||||
name, contents := file[0], file[1]
|
||||
mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(contents))
|
||||
})
|
||||
}
|
||||
|
||||
// This is how httptest.NewServer sets up a net.Listener, except that our listener must accept remote
|
||||
// connections (from the container).
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := httptest.NewUnstartedServer(mux)
|
||||
s.Listener = listener
|
||||
s.Start()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
for _, ctx := range testContexts {
|
||||
buildImage(ctx, t)
|
||||
|
@ -121,9 +211,24 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
|
|||
pullingPool: make(map[string]struct{}),
|
||||
pushingPool: make(map[string]struct{}),
|
||||
}
|
||||
buildfile := NewBuildFile(srv, ioutil.Discard, false)
|
||||
|
||||
id, err := buildfile.Build(mkTestContext(context.dockerfile, context.files, t))
|
||||
httpServer, err := mkTestingFileServer(context.remoteFiles)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer httpServer.Close()
|
||||
|
||||
idx := strings.LastIndex(httpServer.URL, ":")
|
||||
if idx < 0 {
|
||||
t.Fatalf("could not get port from test http server address %s", httpServer.URL)
|
||||
}
|
||||
port := httpServer.URL[idx+1:]
|
||||
|
||||
ip := runtime.networkManager.bridgeNetwork.IP
|
||||
dockerfile := constructDockerfile(context.dockerfile, ip, port)
|
||||
|
||||
buildfile := NewBuildFile(srv, ioutil.Discard, false)
|
||||
id, err := buildfile.Build(mkTestContext(dockerfile, context.files, t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -137,10 +242,10 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
|
|||
|
||||
func TestVolume(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
volume /test
|
||||
cmd Hello world
|
||||
`, nil}, t)
|
||||
`, nil, nil}, t)
|
||||
|
||||
if len(img.Config.Volumes) == 0 {
|
||||
t.Fail()
|
||||
|
@ -154,9 +259,9 @@ func TestVolume(t *testing.T) {
|
|||
|
||||
func TestBuildMaintainer(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
maintainer dockerio
|
||||
`, nil}, t)
|
||||
`, nil, nil}, t)
|
||||
|
||||
if img.Author != "dockerio" {
|
||||
t.Fail()
|
||||
|
@ -165,22 +270,28 @@ func TestBuildMaintainer(t *testing.T) {
|
|||
|
||||
func TestBuildEnv(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
env port 4243
|
||||
`,
|
||||
nil}, t)
|
||||
|
||||
if img.Config.Env[0] != "port=4243" {
|
||||
nil, nil}, t)
|
||||
hasEnv := false
|
||||
for _, envVar := range img.Config.Env {
|
||||
if envVar == "port=4243" {
|
||||
hasEnv = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasEnv {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCmd(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
cmd ["/bin/echo", "Hello World"]
|
||||
`,
|
||||
nil}, t)
|
||||
nil, nil}, t)
|
||||
|
||||
if img.Config.Cmd[0] != "/bin/echo" {
|
||||
t.Log(img.Config.Cmd[0])
|
||||
|
@ -194,10 +305,10 @@ func TestBuildCmd(t *testing.T) {
|
|||
|
||||
func TestBuildExpose(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
expose 4243
|
||||
`,
|
||||
nil}, t)
|
||||
nil, nil}, t)
|
||||
|
||||
if img.Config.PortSpecs[0] != "4243" {
|
||||
t.Fail()
|
||||
|
@ -206,10 +317,10 @@ func TestBuildExpose(t *testing.T) {
|
|||
|
||||
func TestBuildEntrypoint(t *testing.T) {
|
||||
img := buildImage(testContextTemplate{`
|
||||
from %s
|
||||
from {IMAGE}
|
||||
entrypoint ["/bin/echo"]
|
||||
`,
|
||||
nil}, t)
|
||||
nil, nil}, t)
|
||||
|
||||
if img.Config.Entrypoint[0] != "/bin/echo" {
|
||||
}
|
||||
|
|
160
commands.go
160
commands.go
|
@ -78,6 +78,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
|
|||
{"build", "Build a container from a Dockerfile"},
|
||||
{"commit", "Create a new image from a container's changes"},
|
||||
{"diff", "Inspect changes on a container's filesystem"},
|
||||
{"events", "Get real time events from the server"},
|
||||
{"export", "Stream the contents of a container as a tar archive"},
|
||||
{"history", "Show the history of an image"},
|
||||
{"images", "List images"},
|
||||
|
@ -185,6 +186,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
|
||||
isRemote = true
|
||||
} else {
|
||||
if _, err := os.Stat(cmd.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
context, err = Tar(cmd.Arg(0), Uncompressed)
|
||||
}
|
||||
var body io.Reader
|
||||
|
@ -310,16 +314,29 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
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()]
|
||||
if !ok {
|
||||
authconfig = auth.AuthConfig{}
|
||||
}
|
||||
|
||||
if *flUsername == "" {
|
||||
fmt.Fprintf(cli.out, "Username (%s): ", cli.authConfig.Username)
|
||||
promptDefault("Username", authconfig.Username)
|
||||
username = readAndEchoString(cli.in, cli.out)
|
||||
if username == "" {
|
||||
username = cli.authConfig.Username
|
||||
username = authconfig.Username
|
||||
}
|
||||
} else {
|
||||
username = *flUsername
|
||||
}
|
||||
if username != cli.authConfig.Username {
|
||||
if username != authconfig.Username {
|
||||
if *flPassword == "" {
|
||||
fmt.Fprintf(cli.out, "Password: ")
|
||||
password = readString(cli.in, cli.out)
|
||||
|
@ -331,31 +348,30 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
}
|
||||
|
||||
if *flEmail == "" {
|
||||
fmt.Fprintf(cli.out, "Email (%s): ", cli.authConfig.Email)
|
||||
promptDefault("Email", authconfig.Email)
|
||||
email = readAndEchoString(cli.in, cli.out)
|
||||
if email == "" {
|
||||
email = cli.authConfig.Email
|
||||
email = authconfig.Email
|
||||
}
|
||||
} else {
|
||||
email = *flEmail
|
||||
}
|
||||
} else {
|
||||
password = cli.authConfig.Password
|
||||
email = cli.authConfig.Email
|
||||
password = authconfig.Password
|
||||
email = authconfig.Email
|
||||
}
|
||||
if oldState != nil {
|
||||
term.RestoreTerminal(cli.terminalFd, oldState)
|
||||
}
|
||||
cli.authConfig.Username = username
|
||||
cli.authConfig.Password = password
|
||||
cli.authConfig.Email = email
|
||||
authconfig.Username = username
|
||||
authconfig.Password = password
|
||||
authconfig.Email = email
|
||||
cli.configFile.Configs[auth.IndexServerAddress()] = authconfig
|
||||
|
||||
body, statusCode, err := cli.call("POST", "/auth", cli.authConfig)
|
||||
body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()])
|
||||
if statusCode == 401 {
|
||||
cli.authConfig.Username = ""
|
||||
cli.authConfig.Password = ""
|
||||
cli.authConfig.Email = ""
|
||||
auth.SaveConfig(cli.authConfig)
|
||||
delete(cli.configFile.Configs, auth.IndexServerAddress())
|
||||
auth.SaveConfig(cli.configFile)
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -365,10 +381,10 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
var out2 APIAuth
|
||||
err = json.Unmarshal(body, &out2)
|
||||
if err != nil {
|
||||
auth.LoadConfig(os.Getenv("HOME"))
|
||||
cli.configFile, _ = auth.LoadConfig(os.Getenv("HOME"))
|
||||
return err
|
||||
}
|
||||
auth.SaveConfig(cli.authConfig)
|
||||
auth.SaveConfig(cli.configFile)
|
||||
if out2.Status != "" {
|
||||
fmt.Fprintf(cli.out, "%s\n", out2.Status)
|
||||
}
|
||||
|
@ -472,6 +488,9 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
|
|||
fmt.Fprintf(cli.out, "Debug mode (client): %v\n", os.Getenv("DEBUG") != "")
|
||||
fmt.Fprintf(cli.out, "Fds: %d\n", out.NFd)
|
||||
fmt.Fprintf(cli.out, "Goroutines: %d\n", out.NGoroutines)
|
||||
fmt.Fprintf(cli.out, "LXC Version: %s\n", out.LXCVersion)
|
||||
fmt.Fprintf(cli.out, "EventsListeners: %d\n", out.NEventsListener)
|
||||
fmt.Fprintf(cli.out, "Kernel Version: %s\n", out.KernelVersion)
|
||||
}
|
||||
if !out.MemoryLimit {
|
||||
fmt.Fprintf(cli.err, "WARNING: No memory limit support\n")
|
||||
|
@ -484,7 +503,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
|
|||
|
||||
func (cli *DockerCli) CmdStop(args ...string) error {
|
||||
cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container")
|
||||
nSeconds := cmd.Int("t", 10, "Number of seconds to try to stop for before killing the container. Default=10")
|
||||
nSeconds := cmd.Int("t", 10, "Number of seconds to wait for the container to stop before killing it.")
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -594,23 +613,28 @@ func (cli *DockerCli) CmdTop(args ...string) error {
|
|||
if err := cmd.Parse(args); err != nil {
|
||||
return nil
|
||||
}
|
||||
if cmd.NArg() != 1 {
|
||||
if cmd.NArg() == 0 {
|
||||
cmd.Usage()
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
var procs []APITop
|
||||
procs := APITop{}
|
||||
err = json.Unmarshal(body, &procs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
||||
fmt.Fprintln(w, "PID\tTTY\tTIME\tCMD")
|
||||
for _, proc := range procs {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", proc.PID, proc.Tty, proc.Time, proc.Cmd)
|
||||
fmt.Fprintln(w, strings.Join(procs.Titles, "\t"))
|
||||
for _, proc := range procs.Processes {
|
||||
fmt.Fprintln(w, strings.Join(proc, "\t"))
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
|
@ -765,7 +789,7 @@ func (cli *DockerCli) CmdKill(args ...string) error {
|
|||
}
|
||||
|
||||
func (cli *DockerCli) CmdImport(args ...string) error {
|
||||
cmd := Subcmd("import", "URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball")
|
||||
cmd := Subcmd("import", "URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball(.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz).")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return nil
|
||||
|
@ -808,10 +832,10 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
|||
// Custom repositories can have different rules, and we must also
|
||||
// allow pushing by image ID.
|
||||
if len(strings.SplitN(name, "/", 2)) == 1 {
|
||||
return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.authConfig.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)
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(cli.authConfig)
|
||||
buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1061,6 +1085,29 @@ func (cli *DockerCli) CmdCommit(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) CmdEvents(args ...string) error {
|
||||
cmd := Subcmd("events", "[OPTIONS]", "Get real time events from the server")
|
||||
since := cmd.String("since", "", "Show events previously created (used for polling).")
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.NArg() != 0 {
|
||||
cmd.Usage()
|
||||
return nil
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
if *since != "" {
|
||||
v.Set("since", *since)
|
||||
}
|
||||
|
||||
if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) CmdExport(args ...string) error {
|
||||
cmd := Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive")
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
|
@ -1114,10 +1161,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1", false, nil, cli.out); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stderr=1", false, nil, cli.err); err != nil {
|
||||
if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -1320,6 +1364,18 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var containerIDFile *os.File
|
||||
if len(hostConfig.ContainerIDFile) > 0 {
|
||||
if _, err := ioutil.ReadFile(hostConfig.ContainerIDFile); err == nil {
|
||||
return fmt.Errorf("cid file found, make sure the other container isn't running or delete %s", hostConfig.ContainerIDFile)
|
||||
}
|
||||
containerIDFile, err = os.Create(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the container ID file: %s", err)
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
}
|
||||
|
||||
//create the container
|
||||
body, statusCode, err := cli.call("POST", "/containers/create", config)
|
||||
//if image not found try to pull it
|
||||
|
@ -1350,6 +1406,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
for _, warning := range runResult.Warnings {
|
||||
fmt.Fprintf(cli.err, "WARNING: %s\n", warning)
|
||||
}
|
||||
if len(hostConfig.ContainerIDFile) > 0 {
|
||||
if _, err = containerIDFile.WriteString(runResult.ID); err != nil {
|
||||
return fmt.Errorf("failed to write the container ID to the file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
//start the container
|
||||
if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", hostConfig); err != nil {
|
||||
|
@ -1370,7 +1431,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
if config.AttachStdin || config.AttachStdout || config.AttachStderr {
|
||||
if config.Tty {
|
||||
if err := cli.monitorTtySize(runResult.ID); err != nil {
|
||||
return err
|
||||
utils.Debugf("Error monitoring TTY size: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1402,11 +1463,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
|
||||
func (cli *DockerCli) checkIfLogged(action string) error {
|
||||
// If condition AND the login failed
|
||||
if cli.authConfig.Username == "" {
|
||||
if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
|
||||
if err := cli.CmdLogin(""); err != nil {
|
||||
return err
|
||||
}
|
||||
if cli.authConfig.Username == "" {
|
||||
if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
|
||||
return fmt.Errorf("Please login prior to %s. ('docker login')", action)
|
||||
}
|
||||
}
|
||||
|
@ -1501,19 +1562,13 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
|
|||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
for {
|
||||
var m utils.JSONMessage
|
||||
if err := dec.Decode(&m); err == io.EOF {
|
||||
var jm utils.JSONMessage
|
||||
if err := dec.Decode(&jm); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.Progress != "" {
|
||||
fmt.Fprintf(out, "%s %s\r", m.Status, m.Progress)
|
||||
} else if m.Error != "" {
|
||||
return fmt.Errorf(m.Error)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s\n", m.Status)
|
||||
}
|
||||
jm.Display(out)
|
||||
}
|
||||
} else {
|
||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||
|
@ -1547,6 +1602,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
|
|||
|
||||
receiveStdout := utils.Go(func() error {
|
||||
_, err := io.Copy(out, br)
|
||||
utils.Debugf("[hijack] End of stdout")
|
||||
return err
|
||||
})
|
||||
|
||||
|
@ -1561,6 +1617,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
|
|||
sendStdin := utils.Go(func() error {
|
||||
if in != nil {
|
||||
io.Copy(rwc, in)
|
||||
utils.Debugf("[hijack] End of stdin")
|
||||
}
|
||||
if tcpc, ok := rwc.(*net.TCPConn); ok {
|
||||
if err := tcpc.CloseWrite(); err != nil {
|
||||
|
@ -1623,13 +1680,12 @@ func (cli *DockerCli) monitorTtySize(id string) error {
|
|||
}
|
||||
cli.resizeTty(id)
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGWINCH)
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGWINCH)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
if sig == syscall.SIGWINCH {
|
||||
cli.resizeTty(id)
|
||||
}
|
||||
for {
|
||||
<-sigchan
|
||||
cli.resizeTty(id)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
|
@ -1662,11 +1718,11 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
|
|||
err = out
|
||||
}
|
||||
|
||||
authConfig, _ := auth.LoadConfig(os.Getenv("HOME"))
|
||||
configFile, _ := auth.LoadConfig(os.Getenv("HOME"))
|
||||
return &DockerCli{
|
||||
proto: proto,
|
||||
addr: addr,
|
||||
authConfig: authConfig,
|
||||
configFile: configFile,
|
||||
in: in,
|
||||
out: out,
|
||||
err: err,
|
||||
|
@ -1678,7 +1734,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
|
|||
type DockerCli struct {
|
||||
proto string
|
||||
addr string
|
||||
authConfig *auth.AuthConfig
|
||||
configFile *auth.ConfigFile
|
||||
in io.ReadCloser
|
||||
out io.Writer
|
||||
err io.Writer
|
||||
|
|
235
commands_test.go
235
commands_test.go
|
@ -38,7 +38,7 @@ func setTimeout(t *testing.T, msg string, d time.Duration, f func()) {
|
|||
f()
|
||||
c <- false
|
||||
}()
|
||||
if <-c {
|
||||
if <-c && msg != "" {
|
||||
t.Fatal(msg)
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error
|
|||
return nil
|
||||
}
|
||||
|
||||
|
||||
// TestRunHostname checks that 'docker run -h' correctly sets a custom hostname
|
||||
func TestRunHostname(t *testing.T) {
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
@ -74,7 +73,7 @@ func TestRunHostname(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
utils.Debugf("--")
|
||||
|
||||
setTimeout(t, "Reading command output time out", 2*time.Second, func() {
|
||||
cmdOutput, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil {
|
||||
|
@ -91,6 +90,156 @@ func TestRunHostname(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestRunExit(t *testing.T) {
|
||||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
c1 := make(chan struct{})
|
||||
go func() {
|
||||
cli.CmdRun("-i", unitTestImageID, "/bin/cat")
|
||||
close(c1)
|
||||
}()
|
||||
|
||||
setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() {
|
||||
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
container := globalRuntime.List()[0]
|
||||
|
||||
// Closing /bin/cat stdin, expect it to exit
|
||||
if err := stdin.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// as the process exited, CmdRun must finish and unblock. Wait for it
|
||||
setTimeout(t, "Waiting for CmdRun timed out", 10*time.Second, func() {
|
||||
<-c1
|
||||
|
||||
go func() {
|
||||
cli.CmdWait(container.ID)
|
||||
}()
|
||||
|
||||
if _, err := bufio.NewReader(stdout).ReadString('\n'); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Make sure that the client has been disconnected
|
||||
setTimeout(t, "The client should have been disconnected once the remote process exited.", 2*time.Second, func() {
|
||||
// Expecting pipe i/o error, just check that read does not block
|
||||
stdin.Read([]byte{})
|
||||
})
|
||||
|
||||
// Cleanup pipes
|
||||
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Expected behaviour: the process dies when the client disconnects
|
||||
func TestRunDisconnect(t *testing.T) {
|
||||
|
||||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
c1 := make(chan struct{})
|
||||
go func() {
|
||||
// We're simulating a disconnect so the return value doesn't matter. What matters is the
|
||||
// fact that CmdRun returns.
|
||||
cli.CmdRun("-i", unitTestImageID, "/bin/cat")
|
||||
close(c1)
|
||||
}()
|
||||
|
||||
setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() {
|
||||
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Close pipes (simulate disconnect)
|
||||
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// as the pipes are close, we expect the process to die,
|
||||
// therefore CmdRun to unblock. Wait for CmdRun
|
||||
setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() {
|
||||
<-c1
|
||||
})
|
||||
|
||||
// Client disconnect after run -i should cause stdin to be closed, which should
|
||||
// cause /bin/cat to exit.
|
||||
setTimeout(t, "Waiting for /bin/cat to exit timed out", 2*time.Second, func() {
|
||||
container := globalRuntime.List()[0]
|
||||
container.Wait()
|
||||
if container.State.Running {
|
||||
t.Fatalf("/bin/cat is still running after closing stdin")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Expected behaviour: the process dies when the client disconnects
|
||||
func TestRunDisconnectTty(t *testing.T) {
|
||||
|
||||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
c1 := make(chan struct{})
|
||||
go func() {
|
||||
// We're simulating a disconnect so the return value doesn't matter. What matters is the
|
||||
// fact that CmdRun returns.
|
||||
if err := cli.CmdRun("-i", "-t", unitTestImageID, "/bin/cat"); err != nil {
|
||||
utils.Debugf("Error CmdRun: %s\n", err)
|
||||
}
|
||||
|
||||
close(c1)
|
||||
}()
|
||||
|
||||
setTimeout(t, "Waiting for the container to be started timed out", 10*time.Second, func() {
|
||||
for {
|
||||
// Client disconnect after run -i should keep stdin out in TTY mode
|
||||
l := globalRuntime.List()
|
||||
if len(l) == 1 && l[0].State.Running {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
|
||||
// Client disconnect after run -i should keep stdin out in TTY mode
|
||||
container := globalRuntime.List()[0]
|
||||
|
||||
setTimeout(t, "Read/Write assertion timed out", 2000*time.Second, func() {
|
||||
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Close pipes (simulate disconnect)
|
||||
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// In tty mode, we expect the process to stay alive even after client's stdin closes.
|
||||
// Do not wait for run to finish
|
||||
|
||||
// Give some time to monitor to do his thing
|
||||
container.WaitTimeout(500 * time.Millisecond)
|
||||
if !container.State.Running {
|
||||
t.Fatalf("/bin/cat should still be running after closing stdin (tty mode)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAttachStdin checks attaching to stdin without stdout and stderr.
|
||||
// 'docker run -i -a stdin' should sends the client's stdin to the command,
|
||||
|
@ -144,16 +293,88 @@ func TestRunAttachStdin(t *testing.T) {
|
|||
})
|
||||
|
||||
// Check logs
|
||||
if cmdLogs, err := container.ReadLog("stdout"); err != nil {
|
||||
if cmdLogs, err := container.ReadLog("json"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if output, err := ioutil.ReadAll(cmdLogs); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
expectedLog := "hello\nhi there\n"
|
||||
if string(output) != expectedLog {
|
||||
t.Fatalf("Unexpected logs: should be '%s', not '%s'\n", expectedLog, output)
|
||||
expectedLogs := []string{"{\"log\":\"hello\\n\",\"stream\":\"stdout\"", "{\"log\":\"hi there\\n\",\"stream\":\"stdout\""}
|
||||
for _, expectedLog := range expectedLogs {
|
||||
if !strings.Contains(string(output), expectedLog) {
|
||||
t.Fatalf("Unexpected logs: should contains '%s', it is not '%s'\n", expectedLog, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expected behaviour, the process stays alive when the client disconnects
|
||||
func TestAttachDisconnect(t *testing.T) {
|
||||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
go func() {
|
||||
// Start a process in daemon mode
|
||||
if err := cli.CmdRun("-d", "-i", unitTestImageID, "/bin/cat"); err != nil {
|
||||
utils.Debugf("Error CmdRun: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
setTimeout(t, "Waiting for CmdRun timed out", 10*time.Second, func() {
|
||||
if _, err := bufio.NewReader(stdout).ReadString('\n'); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(t, "Waiting for the container to be started timed out", 10*time.Second, func() {
|
||||
for {
|
||||
l := globalRuntime.List()
|
||||
if len(l) == 1 && l[0].State.Running {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
|
||||
container := globalRuntime.List()[0]
|
||||
|
||||
// Attach to it
|
||||
c1 := make(chan struct{})
|
||||
go func() {
|
||||
// We're simulating a disconnect so the return value doesn't matter. What matters is the
|
||||
// fact that CmdAttach returns.
|
||||
cli.CmdAttach(container.ID)
|
||||
close(c1)
|
||||
}()
|
||||
|
||||
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
|
||||
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
// Close pipes (client disconnects)
|
||||
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for attach to finish, the client disconnected, therefore, Attach finished his job
|
||||
setTimeout(t, "Waiting for CmdAttach timed out", 2*time.Second, func() {
|
||||
<-c1
|
||||
})
|
||||
|
||||
// We closed stdin, expect /bin/cat to still be running
|
||||
// Wait a little bit to make sure container.monitor() did his thing
|
||||
err := container.WaitTimeout(500 * time.Millisecond)
|
||||
if err == nil || !container.State.Running {
|
||||
t.Fatalf("/bin/cat is not running after closing stdin")
|
||||
}
|
||||
|
||||
// Try to avoid the timeoout in destroy. Best effort, don't check error
|
||||
cStdin, _ := container.StdinPipe()
|
||||
cStdin.Close()
|
||||
container.Wait()
|
||||
}
|
||||
|
|
183
container.go
183
container.go
|
@ -52,35 +52,37 @@ type Container struct {
|
|||
|
||||
waitLock chan struct{}
|
||||
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 :)
|
||||
VolumesRW map[string]bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Hostname string
|
||||
User string
|
||||
Memory int64 // Memory limit (in bytes)
|
||||
MemorySwap int64 // Total memory usage (memory + swap); set `-1' to disable swap
|
||||
CpuShares int64 // CPU shares (relative weight vs. other containers)
|
||||
AttachStdin bool
|
||||
AttachStdout bool
|
||||
AttachStderr bool
|
||||
PortSpecs []string
|
||||
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
|
||||
OpenStdin bool // Open stdin
|
||||
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
|
||||
Env []string
|
||||
Cmd []string
|
||||
Dns []string
|
||||
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
|
||||
Volumes map[string]struct{}
|
||||
VolumesFrom string
|
||||
Entrypoint []string
|
||||
Hostname string
|
||||
User string
|
||||
Memory int64 // Memory limit (in bytes)
|
||||
MemorySwap int64 // Total memory usage (memory + swap); set `-1' to disable swap
|
||||
CpuShares int64 // CPU shares (relative weight vs. other containers)
|
||||
AttachStdin bool
|
||||
AttachStdout bool
|
||||
AttachStderr bool
|
||||
PortSpecs []string
|
||||
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
|
||||
OpenStdin bool // Open stdin
|
||||
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
|
||||
Env []string
|
||||
Cmd []string
|
||||
Dns []string
|
||||
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
|
||||
Volumes map[string]struct{}
|
||||
VolumesFrom string
|
||||
Entrypoint []string
|
||||
NetworkDisabled bool
|
||||
}
|
||||
|
||||
type HostConfig struct {
|
||||
Binds []string
|
||||
Binds []string
|
||||
ContainerIDFile string
|
||||
}
|
||||
|
||||
type BindMap struct {
|
||||
|
@ -93,16 +95,19 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
|
|||
cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
|
||||
if len(args) > 0 && args[0] != "--help" {
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.Usage = nil
|
||||
}
|
||||
|
||||
flHostname := cmd.String("h", "", "Container host name")
|
||||
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()
|
||||
cmd.Var(flAttach, "a", "Attach to stdin, stdout or stderr.")
|
||||
flStdin := cmd.Bool("i", false, "Keep stdin open even if not attached")
|
||||
flTty := cmd.Bool("t", false, "Allocate a pseudo-tty")
|
||||
flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)")
|
||||
flContainerIDFile := cmd.String("cidfile", "", "Write the container ID to the file")
|
||||
flNetwork := cmd.Bool("n", true, "Enable networking for this container")
|
||||
|
||||
if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit {
|
||||
//fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n")
|
||||
|
@ -171,26 +176,28 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
|
|||
}
|
||||
|
||||
config := &Config{
|
||||
Hostname: *flHostname,
|
||||
PortSpecs: flPorts,
|
||||
User: *flUser,
|
||||
Tty: *flTty,
|
||||
OpenStdin: *flStdin,
|
||||
Memory: *flMemory,
|
||||
CpuShares: *flCpuShares,
|
||||
AttachStdin: flAttach.Get("stdin"),
|
||||
AttachStdout: flAttach.Get("stdout"),
|
||||
AttachStderr: flAttach.Get("stderr"),
|
||||
Env: flEnv,
|
||||
Cmd: runCmd,
|
||||
Dns: flDns,
|
||||
Image: image,
|
||||
Volumes: flVolumes,
|
||||
VolumesFrom: *flVolumesFrom,
|
||||
Entrypoint: entrypoint,
|
||||
Hostname: *flHostname,
|
||||
PortSpecs: flPorts,
|
||||
User: *flUser,
|
||||
Tty: *flTty,
|
||||
NetworkDisabled: !*flNetwork,
|
||||
OpenStdin: *flStdin,
|
||||
Memory: *flMemory,
|
||||
CpuShares: *flCpuShares,
|
||||
AttachStdin: flAttach.Get("stdin"),
|
||||
AttachStdout: flAttach.Get("stdout"),
|
||||
AttachStderr: flAttach.Get("stderr"),
|
||||
Env: flEnv,
|
||||
Cmd: runCmd,
|
||||
Dns: flDns,
|
||||
Image: image,
|
||||
Volumes: flVolumes,
|
||||
VolumesFrom: *flVolumesFrom,
|
||||
Entrypoint: entrypoint,
|
||||
}
|
||||
hostConfig := &HostConfig{
|
||||
Binds: binds,
|
||||
Binds: binds,
|
||||
ContainerIDFile: *flContainerIDFile,
|
||||
}
|
||||
|
||||
if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit {
|
||||
|
@ -372,14 +379,15 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
|||
utils.Debugf("[start] attach stdin\n")
|
||||
defer utils.Debugf("[end] attach stdin\n")
|
||||
// No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr
|
||||
if cStdout != nil {
|
||||
defer cStdout.Close()
|
||||
}
|
||||
if cStderr != nil {
|
||||
defer cStderr.Close()
|
||||
}
|
||||
if container.Config.StdinOnce && !container.Config.Tty {
|
||||
defer cStdin.Close()
|
||||
} else {
|
||||
if cStdout != nil {
|
||||
defer cStdout.Close()
|
||||
}
|
||||
if cStderr != nil {
|
||||
defer cStderr.Close()
|
||||
}
|
||||
}
|
||||
if container.Config.Tty {
|
||||
_, err = utils.CopyEscapable(cStdin, stdin)
|
||||
|
@ -507,8 +515,12 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.allocateNetwork(); err != nil {
|
||||
return err
|
||||
if container.runtime.networkManager.disabled {
|
||||
container.Config.NetworkDisabled = true
|
||||
} else {
|
||||
if err := container.allocateNetwork(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the config is compatible with the current kernel
|
||||
|
@ -520,8 +532,6 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
log.Printf("WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.\n")
|
||||
container.Config.MemorySwap = -1
|
||||
}
|
||||
container.Volumes = make(map[string]string)
|
||||
container.VolumesRW = make(map[string]bool)
|
||||
|
||||
// Create the requested bind mounts
|
||||
binds := make(map[string]BindMap)
|
||||
|
@ -561,30 +571,35 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
|
||||
// FIXME: evaluate volumes-from before individual volumes, so that the latter can override the former.
|
||||
// Create the requested volumes volumes
|
||||
for volPath := range container.Config.Volumes {
|
||||
volPath = path.Clean(volPath)
|
||||
// If an external bind is defined for this volume, use that as a source
|
||||
if bindMap, exists := binds[volPath]; exists {
|
||||
container.Volumes[volPath] = bindMap.SrcPath
|
||||
if strings.ToLower(bindMap.Mode) == "rw" {
|
||||
container.VolumesRW[volPath] = true
|
||||
if container.Volumes == nil || len(container.Volumes) == 0 {
|
||||
container.Volumes = make(map[string]string)
|
||||
container.VolumesRW = make(map[string]bool)
|
||||
|
||||
for volPath := range container.Config.Volumes {
|
||||
volPath = path.Clean(volPath)
|
||||
// If an external bind is defined for this volume, use that as a source
|
||||
if bindMap, exists := binds[volPath]; exists {
|
||||
container.Volumes[volPath] = bindMap.SrcPath
|
||||
if strings.ToLower(bindMap.Mode) == "rw" {
|
||||
container.VolumesRW[volPath] = true
|
||||
}
|
||||
// Otherwise create an directory in $ROOT/volumes/ and use that
|
||||
} else {
|
||||
c, err := container.runtime.volumes.Create(nil, container, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcPath, err := c.layer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
container.Volumes[volPath] = srcPath
|
||||
container.VolumesRW[volPath] = true // RW by default
|
||||
}
|
||||
// Otherwise create an directory in $ROOT/volumes/ and use that
|
||||
} else {
|
||||
c, err := container.runtime.volumes.Create(nil, container, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
// Create the mountpoint
|
||||
if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil {
|
||||
return nil
|
||||
}
|
||||
srcPath, err := c.layer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
container.Volumes[volPath] = srcPath
|
||||
container.VolumesRW[volPath] = true // RW by default
|
||||
}
|
||||
// Create the mountpoint
|
||||
if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -619,7 +634,9 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
}
|
||||
|
||||
// Networking
|
||||
params = append(params, "-g", container.network.Gateway.String())
|
||||
if !container.Config.NetworkDisabled {
|
||||
params = append(params, "-g", container.network.Gateway.String())
|
||||
}
|
||||
|
||||
// User
|
||||
if container.Config.User != "" {
|
||||
|
@ -634,6 +651,7 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
params = append(params,
|
||||
"-e", "HOME=/",
|
||||
"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"-e", "container=lxc",
|
||||
)
|
||||
|
||||
for _, elem := range container.Config.Env {
|
||||
|
@ -647,10 +665,10 @@ func (container *Container) Start(hostConfig *HostConfig) error {
|
|||
container.cmd = exec.Command("lxc-start", params...)
|
||||
|
||||
// Setup logging of stdout and stderr to disk
|
||||
if err := container.runtime.LogToDisk(container.stdout, container.logPath("stdout")); err != nil {
|
||||
if err := container.runtime.LogToDisk(container.stdout, container.logPath("json"), "stdout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.runtime.LogToDisk(container.stderr, container.logPath("stderr")); err != nil {
|
||||
if err := container.runtime.LogToDisk(container.stderr, container.logPath("json"), "stderr"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -709,17 +727,21 @@ func (container *Container) StdinPipe() (io.WriteCloser, error) {
|
|||
|
||||
func (container *Container) StdoutPipe() (io.ReadCloser, error) {
|
||||
reader, writer := io.Pipe()
|
||||
container.stdout.AddWriter(writer)
|
||||
container.stdout.AddWriter(writer, "")
|
||||
return utils.NewBufReader(reader), nil
|
||||
}
|
||||
|
||||
func (container *Container) StderrPipe() (io.ReadCloser, error) {
|
||||
reader, writer := io.Pipe()
|
||||
container.stderr.AddWriter(writer)
|
||||
container.stderr.AddWriter(writer, "")
|
||||
return utils.NewBufReader(reader), nil
|
||||
}
|
||||
|
||||
func (container *Container) allocateNetwork() error {
|
||||
if container.Config.NetworkDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
iface, err := container.runtime.networkManager.Allocate()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -746,6 +768,9 @@ func (container *Container) allocateNetwork() error {
|
|||
}
|
||||
|
||||
func (container *Container) releaseNetwork() {
|
||||
if container.Config.NetworkDisabled {
|
||||
return
|
||||
}
|
||||
container.network.Release()
|
||||
container.network = nil
|
||||
container.NetworkSettings = &NetworkSettings{}
|
||||
|
@ -781,7 +806,9 @@ func (container *Container) monitor() {
|
|||
}
|
||||
}
|
||||
utils.Debugf("Process finished")
|
||||
|
||||
if container.runtime != nil && container.runtime.srv != nil {
|
||||
container.runtime.srv.LogEvent("die", container.ShortID())
|
||||
}
|
||||
exitCode := -1
|
||||
if container.cmd != nil {
|
||||
exitCode = container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
|
||||
|
|
|
@ -39,16 +39,11 @@ func TestIDFormat(t *testing.T) {
|
|||
func TestMultipleAttachRestart(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c",
|
||||
"i=1; while [ $i -le 5 ]; do i=`expr $i + 1`; echo hello; done"},
|
||||
},
|
||||
container, hostConfig, _ := mkContainer(
|
||||
runtime,
|
||||
[]string{"_", "/bin/sh", "-c", "i=1; while [ $i -le 5 ]; do i=`expr $i + 1`; echo hello; done"},
|
||||
t,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
// Simulate 3 client attaching to the container and stop/restart
|
||||
|
@ -65,7 +60,6 @@ func TestMultipleAttachRestart(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -140,19 +134,8 @@ func TestMultipleAttachRestart(t *testing.T) {
|
|||
func TestDiff(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
// Create a container and remove a file
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/rm", "/etc/passwd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container1, _, _ := mkContainer(runtime, []string{"_", "/bin/rm", "/etc/passwd"}, t)
|
||||
defer runtime.Destroy(container1)
|
||||
|
||||
if err := container1.Run(); err != nil {
|
||||
|
@ -185,15 +168,7 @@ func TestDiff(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create a new container from the commited image
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.ID,
|
||||
Cmd: []string{"cat", "/etc/passwd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2, _, _ := mkContainer(runtime, []string{img.ID, "cat", "/etc/passwd"}, t)
|
||||
defer runtime.Destroy(container2)
|
||||
|
||||
if err := container2.Run(); err != nil {
|
||||
|
@ -212,15 +187,7 @@ func TestDiff(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create a new containere
|
||||
container3, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"rm", "/bin/httpd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container3, _, _ := mkContainer(runtime, []string{"_", "rm", "/bin/httpd"}, t)
|
||||
defer runtime.Destroy(container3)
|
||||
|
||||
if err := container3.Run(); err != nil {
|
||||
|
@ -246,17 +213,7 @@ func TestDiff(t *testing.T) {
|
|||
func TestCommitAutoRun(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c", "echo hello > /world"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container1, _, _ := mkContainer(runtime, []string{"_", "/bin/sh", "-c", "echo hello > /world"}, t)
|
||||
defer runtime.Destroy(container1)
|
||||
|
||||
if container1.State.Running {
|
||||
|
@ -279,14 +236,7 @@ func TestCommitAutoRun(t *testing.T) {
|
|||
}
|
||||
|
||||
// FIXME: Make a TestCommit that stops here and check docker.root/layers/img.id/world
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.ID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2, hostConfig, _ := mkContainer(runtime, []string{img.ID}, t)
|
||||
defer runtime.Destroy(container2)
|
||||
stdout, err := container2.StdoutPipe()
|
||||
if err != nil {
|
||||
|
@ -296,7 +246,6 @@ func TestCommitAutoRun(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container2.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -324,17 +273,7 @@ func TestCommitRun(t *testing.T) {
|
|||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c", "echo hello > /world"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container1, hostConfig, _ := mkContainer(runtime, []string{"_", "/bin/sh", "-c", "echo hello > /world"}, t)
|
||||
defer runtime.Destroy(container1)
|
||||
|
||||
if container1.State.Running {
|
||||
|
@ -357,16 +296,7 @@ func TestCommitRun(t *testing.T) {
|
|||
}
|
||||
|
||||
// FIXME: Make a TestCommit that stops here and check docker.root/layers/img.id/world
|
||||
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.ID,
|
||||
Cmd: []string{"cat", "/world"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2, hostConfig, _ := mkContainer(runtime, []string{img.ID, "cat", "/world"}, t)
|
||||
defer runtime.Destroy(container2)
|
||||
stdout, err := container2.StdoutPipe()
|
||||
if err != nil {
|
||||
|
@ -376,7 +306,6 @@ func TestCommitRun(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container2.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -403,18 +332,7 @@ func TestCommitRun(t *testing.T) {
|
|||
func TestStart(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Memory: 33554432,
|
||||
CpuShares: 1000,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container, hostConfig, _ := mkContainer(runtime, []string{"-m", "33554432", "-c", "1000", "-i", "_", "/bin/cat"}, t)
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
cStdin, err := container.StdinPipe()
|
||||
|
@ -422,7 +340,6 @@ func TestStart(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -445,15 +362,7 @@ func TestStart(t *testing.T) {
|
|||
func TestRun(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
if container.State.Running {
|
||||
|
@ -1050,6 +959,7 @@ func TestEnv(t *testing.T) {
|
|||
goodEnv := []string{
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"HOME=/",
|
||||
"container=lxc",
|
||||
}
|
||||
sort.Strings(goodEnv)
|
||||
if len(goodEnv) != len(actualEnv) {
|
||||
|
@ -1299,3 +1209,84 @@ func TestVolumesFromReadonlyMount(t *testing.T) {
|
|||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// Test that restarting a container with a volume does not create a new volume on restart. Regression test for #819.
|
||||
func TestRestartWithVolumes(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "-n", "foobar"},
|
||||
Volumes: map[string]struct{}{"/test": {}},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
for key := range container.Config.Volumes {
|
||||
if key != "/test" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
_, err = container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := container.Volumes["/test"]
|
||||
if expected == "" {
|
||||
t.Fail()
|
||||
}
|
||||
// Run the container again to verify the volume path persists
|
||||
_, err = container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := container.Volumes["/test"]
|
||||
if expected != actual {
|
||||
t.Fatalf("Expected volume path: %s Actual path: %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyLoopbackExistsWhenUsingDisableNetworkOption(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
config, hc, _, err := ParseRun([]string{"-n=false", GetTestImage(runtime).ID, "ip", "addr", "show"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := NewBuilder(runtime).Create(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stdout, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer runtime.Destroy(c)
|
||||
if err := c.Start(hc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.WaitTimeout(500 * time.Millisecond)
|
||||
c.Wait()
|
||||
output, err := ioutil.ReadAll(stdout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
interfaces := regexp.MustCompile(`(?m)^[0-9]+: [a-zA-Z0-9]+`).FindAllString(string(output), -1)
|
||||
if len(interfaces) != 1 {
|
||||
t.Fatalf("Wrong interface count in test container: expected [1: lo], got [%s]", interfaces)
|
||||
}
|
||||
if interfaces[0] != "1: lo" {
|
||||
t.Fatalf("Wrong interface in test container: expected [1: lo], got [%s]", interfaces)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func main() {
|
|||
flDaemon := flag.Bool("d", false, "Daemon mode")
|
||||
flDebug := flag.Bool("D", false, "Debug mode")
|
||||
flAutoRestart := flag.Bool("r", false, "Restart previously running containers")
|
||||
bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge")
|
||||
bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge. Use 'none' to disable container networking")
|
||||
pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID")
|
||||
flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.")
|
||||
flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.")
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
:description: API Documentation for Docker
|
||||
:keywords: API, Docker, rcli, REST, documentation
|
||||
|
||||
.. COMMENT use http://pythonhosted.org/sphinxcontrib-httpdomain/ to
|
||||
.. document the REST API.
|
||||
|
||||
=================
|
||||
Docker Remote API
|
||||
=================
|
||||
|
@ -13,35 +16,61 @@ Docker Remote API
|
|||
|
||||
- The Remote API is replacing rcli
|
||||
- Default port in the docker deamon is 4243
|
||||
- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr
|
||||
- Since API version 1.2, the auth configuration is now handled client side, so the client has to send the authConfig as POST in /images/(name)/push
|
||||
- The API tends to be REST, but for some complex commands, like attach
|
||||
or pull, the HTTP connection is hijacked to transport stdout stdin
|
||||
and stderr
|
||||
- Since API version 1.2, the auth configuration is now handled client
|
||||
side, so the client has to send the authConfig as POST in
|
||||
/images/(name)/push
|
||||
|
||||
2. Versions
|
||||
===========
|
||||
|
||||
The current verson of the API is 1.3
|
||||
Calling /images/<name>/insert is the same as calling /v1.3/images/<name>/insert
|
||||
You can still call an old version of the api using /v1.0/images/<name>/insert
|
||||
The current verson of the API is 1.4
|
||||
|
||||
:doc:`docker_remote_api_v1.3`
|
||||
Calling /images/<name>/insert is the same as calling
|
||||
/v1.4/images/<name>/insert
|
||||
|
||||
You can still call an old version of the api using
|
||||
/v1.0/images/<name>/insert
|
||||
|
||||
:doc:`docker_remote_api_v1.4`
|
||||
*****************************
|
||||
|
||||
What's new
|
||||
----------
|
||||
|
||||
Listing processes (/top):
|
||||
.. http:get:: /containers/(id)/top
|
||||
|
||||
- List the processes 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:
|
||||
|
||||
**New!** Monitor docker's events via streaming or via polling
|
||||
|
||||
Builder (/build):
|
||||
|
||||
- Simplify the upload of the build context
|
||||
- Simply stream a tarball instead of multipart upload with 4 intermediary buffers
|
||||
- Simply stream a tarball instead of multipart upload with 4
|
||||
intermediary buffers
|
||||
- Simpler, less memory usage, less disk usage and faster
|
||||
|
||||
.. Note::
|
||||
The /build improvements are not reverse-compatible. Pre 1.3 clients will break on /build.
|
||||
.. Warning::
|
||||
|
||||
The /build improvements are not reverse-compatible. Pre 1.3 clients
|
||||
will break on /build.
|
||||
|
||||
List containers (/containers/json):
|
||||
|
||||
|
@ -49,7 +78,8 @@ List containers (/containers/json):
|
|||
|
||||
Start containers (/containers/<id>/start):
|
||||
|
||||
- You can now pass host-specific configuration (e.g. bind mounts) in the POST body for start calls
|
||||
- You can now pass host-specific configuration (e.g. bind mounts) in
|
||||
the POST body for start calls
|
||||
|
||||
:doc:`docker_remote_api_v1.2`
|
||||
*****************************
|
||||
|
@ -60,14 +90,25 @@ What's new
|
|||
----------
|
||||
|
||||
The auth configuration is now handled by the client.
|
||||
The client should send it's authConfig as POST on each call of /images/(name)/push
|
||||
|
||||
.. http:get:: /auth is now deprecated
|
||||
.. http:post:: /auth only checks the configuration but doesn't store it on the server
|
||||
The client should send it's authConfig as POST on each call of
|
||||
/images/(name)/push
|
||||
|
||||
Deleting an image is now improved, will only untag the image if it has chidrens and remove all the untagged parents if has any.
|
||||
.. http:get:: /auth
|
||||
|
||||
.. http:post:: /images/<name>/delete now returns a JSON with the list of images deleted/untagged
|
||||
**Deprecated.**
|
||||
|
||||
.. http:post:: /auth
|
||||
|
||||
Only checks the configuration but doesn't store it on the server
|
||||
|
||||
Deleting an image is now improved, will only untag the image if it
|
||||
has chidren and remove all the untagged parents if has any.
|
||||
|
||||
.. http:post:: /images/<name>/delete
|
||||
|
||||
Now returns a JSON structure with the list of images
|
||||
deleted/untagged.
|
||||
|
||||
|
||||
:doc:`docker_remote_api_v1.1`
|
||||
|
@ -82,7 +123,7 @@ What's new
|
|||
.. http:post:: /images/(name)/insert
|
||||
.. http:post:: /images/(name)/push
|
||||
|
||||
Uses json stream instead of HTML hijack, it looks like this:
|
||||
Uses json stream instead of HTML hijack, it looks like this:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
|
@ -109,6 +150,7 @@ Initial version
|
|||
.. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f
|
||||
.. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4
|
||||
.. _2e7649b: https://github.com/dotcloud/docker/commit/2e7649beda7c820793bd46766cbc2cfeace7b168
|
||||
.. _51f6c4a: https://github.com/dotcloud/docker/commit/51f6c4a7372450d164c61e0054daf0223ddbd909
|
||||
|
||||
==================================
|
||||
Docker Remote API Client Libraries
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.. use orphan to suppress "WARNING: document isn't included in any toctree"
|
||||
.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
|
||||
|
||||
:orphan:
|
||||
|
||||
:title: Remote API v1.0
|
||||
:description: API Documentation for Docker
|
||||
:keywords: API, Docker, rcli, REST, documentation
|
||||
|
@ -300,8 +305,8 @@ Start a container
|
|||
:statuscode 500: server error
|
||||
|
||||
|
||||
Stop a contaier
|
||||
***************
|
||||
Stop a container
|
||||
****************
|
||||
|
||||
.. http:post:: /containers/(id)/stop
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.. use orphan to suppress "WARNING: document isn't included in any toctree"
|
||||
.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
|
||||
|
||||
:orphan:
|
||||
|
||||
:title: Remote API v1.1
|
||||
:description: API Documentation for Docker
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.. use orphan to suppress "WARNING: document isn't included in any toctree"
|
||||
.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
|
||||
|
||||
:orphan:
|
||||
|
||||
:title: Remote API v1.2
|
||||
:description: API Documentation for Docker
|
||||
:keywords: API, Docker, rcli, REST, documentation
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.. use orphan to suppress "WARNING: document isn't included in any toctree"
|
||||
.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
|
||||
|
||||
:orphan:
|
||||
|
||||
:title: Remote API v1.3
|
||||
:description: API Documentation for Docker
|
||||
:keywords: API, Docker, rcli, REST, documentation
|
||||
|
@ -984,7 +989,10 @@ Display system-wide information
|
|||
"NFd": 11,
|
||||
"NGoroutines":21,
|
||||
"MemoryLimit":true,
|
||||
"SwapLimit":false
|
||||
"SwapLimit":false,
|
||||
"EventsListeners":"0",
|
||||
"LXCVersion":"0.7.5",
|
||||
"KernelVersion":"3.8.0-19-generic"
|
||||
}
|
||||
|
||||
:statuscode 200: no error
|
||||
|
@ -1054,6 +1062,36 @@ Create a new image from a container's changes
|
|||
:statuscode 500: server error
|
||||
|
||||
|
||||
Monitor Docker's events
|
||||
***********************
|
||||
|
||||
.. http:get:: /events
|
||||
|
||||
Get events from docker, either in real time via streaming, or via polling (using `since`)
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /events?since=1374067924
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{"status":"create","id":"dfdf82bd3881","time":1374067924}
|
||||
{"status":"start","id":"dfdf82bd3881","time":1374067924}
|
||||
{"status":"stop","id":"dfdf82bd3881","time":1374067966}
|
||||
{"status":"destroy","id":"dfdf82bd3881","time":1374067970}
|
||||
|
||||
:query since: timestamp used for polling
|
||||
:statuscode 200: no error
|
||||
:statuscode 500: server error
|
||||
|
||||
|
||||
3. Going further
|
||||
================
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -452,7 +452,7 @@ User Register
|
|||
"username": "foobar"'}
|
||||
|
||||
:jsonparameter email: valid email address, that needs to be confirmed
|
||||
:jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
:jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9\_].
|
||||
:jsonparameter password: min 5 characters
|
||||
|
||||
**Example Response**:
|
||||
|
|
|
@ -367,7 +367,8 @@ POST /v1/users
|
|||
{"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
|
||||
|
||||
**Validation**:
|
||||
- **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
- **username**: min 4 character, max 30 characters, must match the regular
|
||||
expression [a-z0-9\_].
|
||||
- **password**: min 5 characters
|
||||
|
||||
**Valid**: return HTTP 200
|
||||
|
@ -566,4 +567,4 @@ Next request::
|
|||
---------------------
|
||||
|
||||
- 1.0 : May 6th 2013 : initial release
|
||||
- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
|
||||
- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
|
||||
Create a new filesystem image from the contents of a tarball
|
||||
|
||||
At this time, the URL must start with ``http`` and point to a single file archive (.tar, .tar.gz, .bzip)
|
||||
containing a root filesystem. If you would like to import from a local directory or archive,
|
||||
At this time, the URL must start with ``http`` and point to a single file archive
|
||||
(.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz)
|
||||
containing a root filesystem. If you would like to import from a local directory or archive,
|
||||
you can use the ``-`` parameter to take the data from standard in.
|
||||
|
||||
Examples
|
||||
|
@ -30,7 +31,7 @@ Import from a local file
|
|||
Import to docker via pipe and standard in
|
||||
|
||||
``$ cat exampleimage.tgz | docker import - exampleimagelocal``
|
||||
|
||||
|
||||
Import from a local directory
|
||||
.............................
|
||||
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
|
||||
-a=map[]: Attach to stdin, stdout or stderr.
|
||||
-c=0: CPU shares (relative weight)
|
||||
-d=false: Detached mode: leave the container running in the background
|
||||
-cidfile="": Write the container ID to the file
|
||||
-d=false: Detached mode: Run container in the background, print new container id
|
||||
-e=[]: Set environment variables
|
||||
-h="": Container host name
|
||||
-i=false: Keep stdin open even if not attached
|
||||
-m=0: Memory limit (in bytes)
|
||||
-n=true: Enable networking for this container
|
||||
-p=[]: Map a network port to the container
|
||||
-t=false: Allocate a pseudo-tty
|
||||
-u="": Username or UID
|
||||
|
@ -26,3 +28,13 @@
|
|||
-v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]. If "host-dir" is missing, then docker creates a new volume.
|
||||
-volumes-from="": Mount all volumes from the given container.
|
||||
-entrypoint="": Overwrite the default entrypoint set by the image.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -cidfile /tmp/docker_test.cid ubuntu echo "test"
|
||||
|
||||
| This will create a container and print "test" to the console. The cidfile flag makes docker attempt to create a new file and write the container ID to it. If the file exists already, docker will return an error. Docker will close this file when docker run exits.
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
::
|
||||
|
||||
Usage: docker stop [OPTIONS] NAME
|
||||
Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
|
||||
|
||||
Stop a running container
|
||||
|
||||
-t=10: Number of seconds to wait for the container to stop before killing it.
|
||||
|
|
|
@ -37,5 +37,6 @@ Contents:
|
|||
start <command/start>
|
||||
stop <command/stop>
|
||||
tag <command/tag>
|
||||
top <command/top>
|
||||
version <command/version>
|
||||
wait <command/wait>
|
||||
wait <command/wait>
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
|
||||
.. _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
|
||||
----------------
|
||||
|
||||
|
@ -131,60 +127,3 @@ sitting 10 miles away.
|
|||
|
||||
With Standard Containers we can put an end to that embarrassment, by
|
||||
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
|
||||
^^^^^^^^
|
||||
|
||||
|
|
|
@ -46,11 +46,13 @@ in a standard build environment.
|
|||
You can run an interactive session in the newly built container:
|
||||
|
||||
::
|
||||
|
||||
docker run -i -t docker bash
|
||||
|
||||
|
||||
To extract the binaries from the container:
|
||||
|
||||
::
|
||||
|
||||
docker run docker sh -c 'cat $(which docker)' > docker-build && chmod +x docker-build
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ This time, we're requesting shared access to $COUCH1's volumes.
|
|||
|
||||
.. 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
|
||||
----------------------------------
|
||||
|
@ -48,6 +48,6 @@ Browse data on the second database
|
|||
|
||||
HOST=localhost
|
||||
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.
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
:description: An overview of the Docker Documentation
|
||||
:keywords: containers, lxc, concepts, explanation
|
||||
|
||||
.. _introduction:
|
||||
|
||||
Welcome
|
||||
=======
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ For example:
|
|||
.. code-block:: bash
|
||||
|
||||
# Run docker in daemon mode
|
||||
sudo <path to>/docker -H 0.0.0.0:5555 &
|
||||
sudo <path to>/docker -H 0.0.0.0:5555 -d &
|
||||
# Download a base image
|
||||
docker -H :5555 pull base
|
||||
|
||||
|
@ -61,7 +61,7 @@ on both tcp and a unix socket
|
|||
.. code-block:: bash
|
||||
|
||||
# Run docker in daemon mode
|
||||
sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock
|
||||
sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock -d &
|
||||
# Download a base image
|
||||
docker pull base
|
||||
# OR
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
:title: Dockerfile Builder
|
||||
:description: Docker Builder specifes a simple DSL which allows you to automate the steps you would normally manually take to create an image.
|
||||
:keywords: builder, docker, Docker Builder, automation, image creation
|
||||
:title: Dockerfiles for Images
|
||||
:description: Dockerfiles use a simple DSL which allows you to automate the steps you would normally manually take to create an image.
|
||||
:keywords: builder, docker, Dockerfile, automation, image creation
|
||||
|
||||
==================
|
||||
Dockerfile Builder
|
||||
|
@ -30,7 +30,7 @@ build succeeds:
|
|||
|
||||
``docker build -t shykes/myapp .``
|
||||
|
||||
Docker will run your steps one-by-one, committing the result if necessary,
|
||||
Docker will run your steps one-by-one, committing the result if necessary,
|
||||
before finally outputting the ID of your new image.
|
||||
|
||||
2. Format
|
||||
|
@ -43,7 +43,7 @@ The Dockerfile format is quite simple:
|
|||
# Comment
|
||||
INSTRUCTION arguments
|
||||
|
||||
The Instruction is not case-sensitive, however convention is for them to be
|
||||
The Instruction is not case-sensitive, however convention is for them to be
|
||||
UPPERCASE in order to distinguish them from arguments more easily.
|
||||
|
||||
Docker evaluates the instructions in a Dockerfile in order. **The first
|
||||
|
@ -106,7 +106,7 @@ The ``CMD`` instruction sets the command to be executed when running
|
|||
the image. This is functionally equivalent to running ``docker commit
|
||||
-run '{"Cmd": <command>}'`` outside the builder.
|
||||
|
||||
.. note::
|
||||
.. note::
|
||||
Don't confuse `RUN` with `CMD`. `RUN` actually runs a
|
||||
command and commits the result; `CMD` does not execute anything at
|
||||
build time, but specifies the intended command for the image.
|
||||
|
@ -131,7 +131,7 @@ value ``<value>``. This value will be passed to all future ``RUN``
|
|||
instructions. This is functionally equivalent to prefixing the command
|
||||
with ``<key>=<value>``
|
||||
|
||||
.. note::
|
||||
.. note::
|
||||
The environment variables will persist when a container is run
|
||||
from the resulting image.
|
||||
|
||||
|
@ -152,16 +152,24 @@ destination container.
|
|||
|
||||
The copy obeys the following rules:
|
||||
|
||||
* If ``<src>`` is a URL and ``<dest>`` does not end with a trailing slash,
|
||||
then a file is downloaded from the URL and copied to ``<dest>``.
|
||||
* If ``<src>`` is a URL and ``<dest>`` does end with a trailing slash,
|
||||
then the filename is inferred from the URL and the file is downloaded to
|
||||
``<dest>/<filename>``. For instance, ``ADD http://example.com/foobar /``
|
||||
would create the file ``/foobar``. The URL must have a nontrivial path
|
||||
so that an appropriate filename can be discovered in this case
|
||||
(``http://example.com`` will not work).
|
||||
* If ``<src>`` is a directory, the entire directory is copied,
|
||||
including filesystem metadata.
|
||||
* If ``<src>``` is a tar archive in a recognized compression format
|
||||
(identity, gzip, bzip2 or xz), it is unpacked as a directory.
|
||||
|
||||
When a directory is copied or unpacked, it has the same behavior as
|
||||
``tar -x``: the result is the union of
|
||||
``tar -x``: the result is the union of
|
||||
|
||||
1. whatever existed at the destination path and
|
||||
2. the contents of the source tree,
|
||||
2. the contents of the source tree,
|
||||
|
||||
with conflicts resolved in favor of 2) on a file-by-file basis.
|
||||
|
||||
|
@ -177,9 +185,9 @@ The copy obeys the following rules:
|
|||
with mode 0700, uid and gid 0.
|
||||
|
||||
3.8 ENTRYPOINT
|
||||
-------------
|
||||
--------------
|
||||
|
||||
``ENTRYPOINT /bin/echo``
|
||||
``ENTRYPOINT ["/bin/echo"]``
|
||||
|
||||
The ``ENTRYPOINT`` instruction adds an entry command that will not be
|
||||
overwritten when arguments are passed to docker run, unlike the
|
||||
|
@ -203,14 +211,14 @@ container created from the image.
|
|||
# Nginx
|
||||
#
|
||||
# VERSION 0.0.1
|
||||
|
||||
|
||||
FROM ubuntu
|
||||
MAINTAINER Guillaume J. Charmes "guillaume@dotcloud.com"
|
||||
|
||||
|
||||
# make sure the package repository is up to date
|
||||
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
|
||||
RUN apt-get update
|
||||
|
||||
|
||||
RUN apt-get install -y inotify-tools nginx apache2 openssh-server
|
||||
|
||||
.. code-block:: bash
|
||||
|
@ -218,12 +226,12 @@ container created from the image.
|
|||
# Firefox over VNC
|
||||
#
|
||||
# VERSION 0.3
|
||||
|
||||
|
||||
FROM ubuntu
|
||||
# make sure the package repository is up to date
|
||||
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
|
||||
RUN apt-get update
|
||||
|
||||
|
||||
# Install vnc, xvfb in order to create a 'fake' display and firefox
|
||||
RUN apt-get install -y x11vnc xvfb firefox
|
||||
RUN mkdir /.vnc
|
||||
|
@ -231,7 +239,7 @@ container created from the image.
|
|||
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
|
||||
# Autostart firefox (might not be the best way, but it does the trick)
|
||||
RUN bash -c 'echo "firefox" >> /.bashrc'
|
||||
|
||||
|
||||
EXPOSE 5900
|
||||
CMD ["x11vnc", "-forever", "-usepw", "-create"]
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ your container to an image within your username namespace.
|
|||
|
||||
|
||||
Pushing a container to its repository
|
||||
------------------------------------
|
||||
-------------------------------------
|
||||
|
||||
In order to push an image to its repository you need to have committed
|
||||
your container to a named image (see above)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Thatcher Penskens <thatcher@dotcloud.com>
|
||||
Thatcher Peskens <thatcher@dotcloud.com>
|
||||
|
|
|
@ -68,19 +68,18 @@
|
|||
|
||||
<div style="float: right" class="pull-right">
|
||||
<ul class="nav">
|
||||
<li id="nav-introduction"><a href="http://www.docker.io/">Introduction</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/" title="About">About</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-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>
|
||||
</ul>
|
||||
<!--<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">-->
|
||||
<!--<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>-->
|
||||
<!--<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>-->
|
||||
<!--</div>-->
|
||||
</div>
|
||||
|
||||
<div style="margin-left: -12px; float: left;">
|
||||
<a href="http://www.docker.io"><img style="margin-top: 12px; height: 38px" src="{{ pathto('_static/img/docker-letters-logo.gif', 1) }}"></a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -96,7 +95,7 @@
|
|||
<div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
|
||||
<a href="http://github.com/dotcloud/docker/"><img src="{{ pathto('_static/img/fork-us.png', 1) }}"> Fork us on Github</a>
|
||||
</div>
|
||||
<h1 class="pageheader">DOCUMENTATION</h1>
|
||||
<h1 class="pageheader"><a href="http://docs.docker.io/en/latest/" title="Documentation" style="color: white;">DOCUMENTATION</a></h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,12 +34,12 @@ h4 {
|
|||
.navbar .nav li a {
|
||||
padding: 22px 15px 22px;
|
||||
}
|
||||
.navbar .brand {
|
||||
padding: 13px 10px 13px 28px ;
|
||||
}
|
||||
.navbar-dotcloud .container {
|
||||
border-bottom: 2px #000000 solid;
|
||||
}
|
||||
.inline-icon {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
/*
|
||||
* Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
|
||||
* http://www.jonsuh.com
|
||||
|
@ -82,7 +82,7 @@ h4 {
|
|||
.btn-custom {
|
||||
background-color: #292929 !important;
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:dximagetransform.microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
|
||||
background-image: -khtml-gradient(linear, left top, left bottom, from(#515151), to(#282828));
|
||||
background-image: -moz-linear-gradient(top, #515151, #282828);
|
||||
background-image: -ms-linear-gradient(top, #515151, #282828);
|
||||
|
@ -301,7 +301,7 @@ section.header {
|
|||
height: 28px;
|
||||
line-height: 28px;
|
||||
background-color: #43484c;
|
||||
filter: progid:dximagetransform.microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
|
||||
background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
|
|
|
@ -53,13 +53,6 @@ h1, h2, h3, h4 {
|
|||
padding: 22px 15px 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.brand {
|
||||
padding: 13px 10px 13px 28px ;
|
||||
// padding-left: 30px;
|
||||
|
||||
}
|
||||
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
@ -67,6 +60,9 @@ h1, h2, h3, h4 {
|
|||
border-bottom: 2px @black solid;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.3 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 2.8 KiB |
|
@ -1 +0,0 @@
|
|||
Thatcher Penskens <thatcher@dotcloud.com>
|
|
@ -1,2 +0,0 @@
|
|||
www:
|
||||
type: static
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
<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. They’re 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>
|
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
../theme/docker/static
|
67
graph.go
67
graph.go
|
@ -1,9 +1,7 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/registry"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -11,17 +9,13 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Graph is a store for versioned filesystem images and the relationship between them.
|
||||
type Graph struct {
|
||||
Root string
|
||||
idIndex *utils.TruncIndex
|
||||
checksumLock map[string]*sync.Mutex
|
||||
lockSumFile *sync.Mutex
|
||||
lockSumMap *sync.Mutex
|
||||
Root string
|
||||
idIndex *utils.TruncIndex
|
||||
}
|
||||
|
||||
// NewGraph instantiates a new graph at the given root path in the filesystem.
|
||||
|
@ -36,11 +30,8 @@ func NewGraph(root string) (*Graph, error) {
|
|||
return nil, err
|
||||
}
|
||||
graph := &Graph{
|
||||
Root: abspath,
|
||||
idIndex: utils.NewTruncIndex(),
|
||||
checksumLock: make(map[string]*sync.Mutex),
|
||||
lockSumFile: &sync.Mutex{},
|
||||
lockSumMap: &sync.Mutex{},
|
||||
Root: abspath,
|
||||
idIndex: utils.NewTruncIndex(),
|
||||
}
|
||||
if err := graph.restore(); err != nil {
|
||||
return nil, err
|
||||
|
@ -99,11 +90,6 @@ func (graph *Graph) Get(name string) (*Image, error) {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -123,16 +109,15 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut
|
|||
img.Container = container.ID
|
||||
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
|
||||
}
|
||||
go img.Checksum()
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Register imports a pre-existing image into the graph.
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -145,7 +130,7 @@ func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
// Commit
|
||||
|
@ -154,7 +139,6 @@ func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
|
|||
}
|
||||
img.graph = graph
|
||||
graph.idIndex.Add(img.ID)
|
||||
graph.checksumLock[img.ID] = &sync.Mutex{}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -311,40 +295,3 @@ func (graph *Graph) Heads() (map[string]*Image, error) {
|
|||
func (graph *Graph) imageRoot(id string) string {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ func TestInterruptedRegister(t *testing.T) {
|
|||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
go graph.Register(badArchive, false, image)
|
||||
go graph.Register(nil, badArchive, image)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling)
|
||||
if _, err := graph.Get(image.ID); err == nil {
|
||||
|
@ -49,7 +49,7 @@ func TestInterruptedRegister(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := graph.Register(goodArchive, false, image); err != nil {
|
||||
if err := graph.Register(nil, goodArchive, image); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func TestRegister(t *testing.T) {
|
|||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
err = graph.Register(archive, false, image)
|
||||
err = graph.Register(nil, archive, image)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ func TestDelete(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
if err := graph.Delete(img1.ID); err != nil {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
# Docker maintainer bootcamp
|
||||
|
||||
## Introduction: we need more maintainers
|
||||
|
||||
Docker is growing incredibly fast. At the time of writing, it has received over 200 contributions from 90 people,
|
||||
and its API is used by dozens of 3rd-party tools. Over 1,000 issues have been opened. As the first production deployments
|
||||
start going live, the growth will only accelerate.
|
||||
|
||||
Also at the time of writing, Docker has 3 full-time maintainers, and 7 part-time subsystem maintainers. If docker
|
||||
is going to live up to the expectations, we need more than that.
|
||||
|
||||
This document describes a *bootcamp* to guide and train volunteers interested in helping the project, either with individual
|
||||
contributions, maintainer work, or both.
|
||||
|
||||
This bootcamp is an experiment. If you decide to go through it, consider yourself an alpha-tester. You should expect quirks,
|
||||
and report them to us as you encounter them to help us smooth out the process.
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The maintainer bootcamp is a 12-step program - one step for each of the maintainer's responsibilities. The aspiring maintainer must
|
||||
validate all 12 steps by 1) studying it, 2) practicing it, and 3) getting endorsed for it.
|
||||
|
||||
Steps are all equally important and can be validated in any order. Validating all 12 steps is a pre-requisite for becoming a core
|
||||
maintainer, but even 1 step will make you a better contributor!
|
||||
|
||||
### List of steps
|
||||
|
||||
#### 1) Be a power user
|
||||
|
||||
Use docker daily, build cool things with it, know its quirks inside and out.
|
||||
|
||||
|
||||
#### 2) Help users
|
||||
|
||||
Answer questions on irc, twitter, email, in person.
|
||||
|
||||
|
||||
#### 3) Manage the bug tracker
|
||||
|
||||
Help triage tickets - ask the right questions, find duplicates, reference relevant resources, know when to close a ticket when necessary, take the time to go over older tickets.
|
||||
|
||||
|
||||
#### 4) Improve the documentation
|
||||
|
||||
Follow the documentation from scratch regularly and make sure it is still up-to-date. Find and fix inconsistencies. Remove stale information. Find a frequently asked question that is not documented. Simplify the content and the form.
|
||||
|
||||
|
||||
#### 5) Evangelize the principles of docker
|
||||
|
||||
Understand what the underlying goals and principle of docker are. Explain design decisions based on what docker is, and what it is not. When someone is not using docker, find how docker can be valuable to them. If they are using docker, find how they can use it better.
|
||||
|
||||
|
||||
#### 6) Fix bugs
|
||||
|
||||
Self-explanatory. Contribute improvements to docker which solve defects. Bugfixes should be well-tested, and prioritized by impact to the user.
|
||||
|
||||
|
||||
#### 7) Improve the testing infrastructure
|
||||
|
||||
Automated testing is complicated and should be perpetually improved. Invest time to improve the current tooling. Refactor existing tests, create new ones, make testing more accessible to developers, add new testing capabilities (integration tests, mocking, stress test...), improve integration between tests and documentation...
|
||||
|
||||
|
||||
#### 8) Contribute features
|
||||
|
||||
Improve docker to do more things, or get better at doing the same things. Features should be well-tested, not break existing APIs, respect the project goals. They should make the user's life measurably better. Features should be discussed ahead of time to avoid wasting time and duplicating effort.
|
||||
|
||||
|
||||
#### 9) Refactor internals
|
||||
|
||||
Improve docker to repay technical debt. Simplify code layout, improve performance, add missing comments, reduce the number of files and functions, rename functions and variables to be more readable, go over FIXMEs, etc.
|
||||
|
||||
#### 10) Review and merge contributions
|
||||
|
||||
Review pull requests in a timely manner, review code in detail and offer feedback. Keep a high bar without being pedantic. Share the load of testing and merging pull requests.
|
||||
|
||||
#### 11) Release
|
||||
|
||||
Manage a release of docker from beginning to end. Tests, final review, tags, builds, upload to mirrors, distro packaging, etc.
|
||||
|
||||
#### 12) Train other maintainers
|
||||
|
||||
Contribute to training other maintainers. Give advice, delegate work, help organize the bootcamp. This also means contribute to the maintainer's manual, look for ways to improve the project organization etc.
|
||||
|
||||
### How to study a step
|
||||
|
||||
### How to practice a step
|
||||
|
||||
### How to get endorsed for a step
|
||||
|
||||
|
144
image.go
144
image.go
|
@ -2,7 +2,6 @@ package docker
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -14,6 +13,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -47,6 +47,19 @@ func LoadImage(root string) (*Image, error) {
|
|||
if err := ValidateID(img.ID); err != nil {
|
||||
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
|
||||
if stat, err := os.Stat(layerPath(root)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
@ -59,7 +72,7 @@ func LoadImage(root string) (*Image, error) {
|
|||
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
|
||||
if _, err := os.Stat(root); err == nil {
|
||||
return fmt.Errorf("Image %s already exists", img.ID)
|
||||
|
@ -68,30 +81,10 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error {
|
|||
}
|
||||
// Store the layer
|
||||
layer := layerPath(root)
|
||||
if err := os.MkdirAll(layer, 0700); err != nil {
|
||||
if err := os.MkdirAll(layer, 0755); err != nil {
|
||||
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 != nil {
|
||||
start := time.Now()
|
||||
|
@ -102,25 +95,36 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error {
|
|||
utils.Debugf("Untar time: %vs\n", time.Now().Sub(start).Seconds())
|
||||
}
|
||||
|
||||
// If raw json is provided, then use it
|
||||
if jsonData != nil {
|
||||
return ioutil.WriteFile(jsonPath(root), jsonData, 0600)
|
||||
} else { // Otherwise, unmarshal the image
|
||||
jsonData, err := json.Marshal(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
|
||||
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 {
|
||||
img.Size += fileInfo.Size()
|
||||
totalSize += fileInfo.Size()
|
||||
return nil
|
||||
})
|
||||
img.Size = totalSize
|
||||
|
||||
// Store the json ball
|
||||
jsonData, err := json.Marshal(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
|
||||
return err
|
||||
if err := ioutil.WriteFile(path.Join(root, "layersize"), []byte(strconv.Itoa(int(totalSize))), 0600); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -128,10 +132,6 @@ func layerPath(root string) string {
|
|||
return path.Join(root, "layer")
|
||||
}
|
||||
|
||||
func layerArchivePath(root string) string {
|
||||
return path.Join(root, "layer.tar.xz")
|
||||
}
|
||||
|
||||
func jsonPath(root string) string {
|
||||
return path.Join(root, "json")
|
||||
}
|
||||
|
@ -308,80 +308,6 @@ func (img *Image) layer() (string, error) {
|
|||
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 {
|
||||
parentImage, err := img.GetParent()
|
||||
if err != nil || parentImage == nil {
|
||||
|
|
|
@ -13,6 +13,10 @@ lxc.utsname = {{.Id}}
|
|||
{{end}}
|
||||
#lxc.aa_profile = unconfined
|
||||
|
||||
{{if .Config.NetworkDisabled}}
|
||||
# network is disabled (-n=false)
|
||||
lxc.network.type = empty
|
||||
{{else}}
|
||||
# network configuration
|
||||
lxc.network.type = veth
|
||||
lxc.network.flags = up
|
||||
|
@ -20,6 +24,7 @@ lxc.network.link = {{.NetworkSettings.Bridge}}
|
|||
lxc.network.name = eth0
|
||||
lxc.network.mtu = 1500
|
||||
lxc.network.ipv4 = {{.NetworkSettings.IPAddress}}/{{.NetworkSettings.IPPrefixLen}}
|
||||
{{end}}
|
||||
|
||||
# root filesystem
|
||||
{{$ROOTFS := .RootfsPath}}
|
||||
|
|
52
network.go
52
network.go
|
@ -17,6 +17,7 @@ var NetworkBridgeIface string
|
|||
|
||||
const (
|
||||
DefaultNetworkBridge = "docker0"
|
||||
DisableNetworkBridge = "none"
|
||||
portRangeStart = 49153
|
||||
portRangeEnd = 65535
|
||||
)
|
||||
|
@ -111,10 +112,29 @@ func checkRouteOverlaps(dockerNetwork *net.IPNet) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateBridgeIface creates a network bridge interface on the host system with the name `ifaceName`,
|
||||
// and attempts to configure it with an address which doesn't conflict with any other interface on the host.
|
||||
// If it can't find an address which doesn't conflict, it will return an error.
|
||||
func CreateBridgeIface(ifaceName string) error {
|
||||
// FIXME: try more IP ranges
|
||||
// FIXME: try bigger ranges! /24 is too small.
|
||||
addrs := []string{"172.16.42.1/24", "10.0.42.1/24", "192.168.42.1/24"}
|
||||
addrs := []string{
|
||||
// Here we don't follow the convention of using the 1st IP of the range for the gateway.
|
||||
// This is to use the same gateway IPs as the /24 ranges, which predate the /16 ranges.
|
||||
// In theory this shouldn't matter - in practice there's bound to be a few scripts relying
|
||||
// on the internal addressing or other stupid things like that.
|
||||
// The shouldn't, but hey, let's not break them unless we really have to.
|
||||
"172.16.42.1/16",
|
||||
"10.0.42.1/16", // Don't even try using the entire /8, that's too intrusive
|
||||
"10.1.42.1/16",
|
||||
"10.42.42.1/16",
|
||||
"172.16.42.1/24",
|
||||
"172.16.43.1/24",
|
||||
"172.16.44.1/24",
|
||||
"10.0.42.1/24",
|
||||
"10.0.43.1/24",
|
||||
"192.168.42.1/24",
|
||||
"192.168.43.1/24",
|
||||
"192.168.44.1/24",
|
||||
}
|
||||
|
||||
var ifaceAddr string
|
||||
for _, addr := range addrs {
|
||||
|
@ -453,10 +473,16 @@ type NetworkInterface struct {
|
|||
|
||||
manager *NetworkManager
|
||||
extPorts []*Nat
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// Allocate an external TCP port and map it to the interface
|
||||
func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) {
|
||||
|
||||
if iface.disabled {
|
||||
return nil, fmt.Errorf("Trying to allocate port for interface %v, which is disabled", iface) // FIXME
|
||||
}
|
||||
|
||||
nat, err := parseNat(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -552,6 +578,11 @@ func parseNat(spec string) (*Nat, error) {
|
|||
|
||||
// Release: Network cleanup - release all resources
|
||||
func (iface *NetworkInterface) Release() {
|
||||
|
||||
if iface.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
for _, nat := range iface.extPorts {
|
||||
utils.Debugf("Unmaping %v/%v", nat.Proto, nat.Frontend)
|
||||
if err := iface.manager.portMapper.Unmap(nat.Frontend, nat.Proto); err != nil {
|
||||
|
@ -579,10 +610,17 @@ type NetworkManager struct {
|
|||
tcpPortAllocator *PortAllocator
|
||||
udpPortAllocator *PortAllocator
|
||||
portMapper *PortMapper
|
||||
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// Allocate a network interface
|
||||
func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
|
||||
|
||||
if manager.disabled {
|
||||
return &NetworkInterface{disabled: true}, nil
|
||||
}
|
||||
|
||||
ip, err := manager.ipAllocator.Acquire()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -596,6 +634,14 @@ func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
|
|||
}
|
||||
|
||||
func newNetworkManager(bridgeIface string) (*NetworkManager, error) {
|
||||
|
||||
if bridgeIface == DisableNetworkBridge {
|
||||
manager := &NetworkManager{
|
||||
disabled: true,
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
addr, err := getIfaceAddr(bridgeIface)
|
||||
if err != nil {
|
||||
// If the iface is not found, try to create it
|
||||
|
|
|
@ -923,6 +923,12 @@ List images
|
|||
Usage: docker import [OPTIONS] URL|\- [REPOSITORY [TAG]]
|
||||
.sp
|
||||
Create a new filesystem image from the contents of a tarball
|
||||
|
||||
At this time, the URL must start with ``http`` and point to a single file archive
|
||||
(.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz)
|
||||
containing a root filesystem. If you would like to import from a local directory or archive,
|
||||
you can use the ``-`` parameter to take the data from standard in.
|
||||
|
||||
.SS info
|
||||
.sp
|
||||
.nf
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
description "Run docker"
|
||||
|
||||
start on runlevel [2345]
|
||||
stop on starting rc RUNLEVEL=[016]
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn
|
||||
|
||||
script
|
||||
/usr/bin/docker -d
|
||||
end script
|
||||
exec /usr/bin/docker -d
|
||||
|
|
|
@ -17,8 +17,10 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
var ErrAlreadyExists = errors.New("Image already exists")
|
||||
var ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("Image already exists")
|
||||
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
||||
)
|
||||
|
||||
func pingRegistryEndpoint(endpoint string) error {
|
||||
if endpoint == auth.IndexServerAddress() {
|
||||
|
@ -98,11 +100,39 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
|
|||
return endpoint, reposName, err
|
||||
}
|
||||
|
||||
// VersionInfo is used to model entities which has a version.
|
||||
// It is basically a tupple with name and version.
|
||||
type VersionInfo interface {
|
||||
Name() string
|
||||
Version() string
|
||||
}
|
||||
|
||||
func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
|
||||
for _, cookie := range c.Jar.Cookies(req.URL) {
|
||||
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
|
||||
// in NewRegistry() and extra.
|
||||
func (r *Registry) setUserAgent(req *http.Request, extra ...VersionInfo) {
|
||||
if len(r.baseVersions)+len(extra) == 0 {
|
||||
return
|
||||
}
|
||||
if len(extra) == 0 {
|
||||
req.Header.Set("User-Agent", r.baseVersionsStr)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", appendVersions(r.baseVersionsStr, extra...))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the history of a given image from the Registry.
|
||||
|
@ -113,7 +143,8 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s
|
|||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
|
||||
res, err := r.client.Do(req)
|
||||
r.setUserAgent(req)
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
if res != nil {
|
||||
return nil, fmt.Errorf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID)
|
||||
|
@ -159,7 +190,8 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([
|
|||
return nil, -1, fmt.Errorf("Failed to download json: %s", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
|
||||
res, err := r.client.Do(req)
|
||||
r.setUserAgent(req)
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("Failed to download json: %s", err)
|
||||
}
|
||||
|
@ -186,7 +218,8 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) (
|
|||
return nil, fmt.Errorf("Error while getting from the server: %s\n", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
|
||||
res, err := r.client.Do(req)
|
||||
r.setUserAgent(req)
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -206,7 +239,8 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
|
|||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
|
||||
res, err := r.client.Do(req)
|
||||
r.setUserAgent(req)
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -234,8 +268,11 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
|
|||
}
|
||||
|
||||
func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, error) {
|
||||
|
||||
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote)
|
||||
|
||||
utils.Debugf("[registry] Calling GET %s", repositoryTarget)
|
||||
|
||||
req, err := r.opaqueRequest("GET", repositoryTarget, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -244,6 +281,7 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
|
|||
req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
|
||||
}
|
||||
req.Header.Set("X-Docker-Token", "true")
|
||||
r.setUserAgent(req)
|
||||
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
|
@ -297,18 +335,17 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Push a local image to the registry
|
||||
func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string, token []string) error {
|
||||
// FIXME: try json with UTF8
|
||||
req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", strings.NewReader(string(jsonRaw)))
|
||||
func (r *Registry) PushImageChecksumRegistry(imgData *ImgData, registry string, token []string) error {
|
||||
|
||||
utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum")
|
||||
|
||||
req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
|
||||
req.Header.Set("X-Docker-Checksum", imgData.Checksum)
|
||||
|
||||
utils.Debugf("Setting checksum for %s: %s", imgData.ID, imgData.Checksum)
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to upload metadata: %s", err)
|
||||
|
@ -333,28 +370,68 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, token []string) error {
|
||||
req, err := http.NewRequest("PUT", registry+"images/"+imgID+"/layer", layer)
|
||||
// Push a local image to the registry
|
||||
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 {
|
||||
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 fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err)
|
||||
}
|
||||
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 fmt.Errorf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody)
|
||||
}
|
||||
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.TransferEncoding = []string{"chunked"}
|
||||
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 layer: %s", err)
|
||||
return "", fmt.Errorf("Failed to upload layer: %s", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
errBody, err := ioutil.ReadAll(res.Body)
|
||||
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 "", fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err)
|
||||
}
|
||||
return fmt.Errorf("Received HTTP code %d while uploading layer: %s", res.StatusCode, errBody)
|
||||
return "", fmt.Errorf("Received HTTP code %d while uploading layer: %s", res.StatusCode, errBody)
|
||||
}
|
||||
return nil
|
||||
return tarsumLayer.Sum(jsonRaw), nil
|
||||
}
|
||||
|
||||
func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
|
||||
|
@ -378,6 +455,7 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token
|
|||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
|
||||
r.setUserAgent(req)
|
||||
req.ContentLength = int64(len(revision))
|
||||
res, err := doWithCookies(r.client, req)
|
||||
if err != nil {
|
||||
|
@ -391,7 +469,19 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token
|
|||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -401,7 +491,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
|
|||
}
|
||||
|
||||
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)
|
||||
req, err := r.opaqueRequest("PUT", u, bytes.NewReader(imgListJSON))
|
||||
if err != nil {
|
||||
|
@ -410,6 +500,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
|
|||
req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
|
||||
req.ContentLength = int64(len(imgListJSON))
|
||||
req.Header.Set("X-Docker-Token", "true")
|
||||
r.setUserAgent(req)
|
||||
if validate {
|
||||
req.Header["X-Docker-Endpoints"] = regs
|
||||
}
|
||||
|
@ -430,6 +521,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
|
|||
req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
|
||||
req.ContentLength = int64(len(imgListJSON))
|
||||
req.Header.Set("X-Docker-Token", "true")
|
||||
r.setUserAgent(req)
|
||||
if validate {
|
||||
req.Header["X-Docker-Endpoints"] = regs
|
||||
}
|
||||
|
@ -536,11 +628,52 @@ type ImgData struct {
|
|||
}
|
||||
|
||||
type Registry struct {
|
||||
client *http.Client
|
||||
authConfig *auth.AuthConfig
|
||||
client *http.Client
|
||||
authConfig *auth.AuthConfig
|
||||
baseVersions []VersionInfo
|
||||
baseVersionsStr string
|
||||
}
|
||||
|
||||
func NewRegistry(root string, authConfig *auth.AuthConfig) (r *Registry, err error) {
|
||||
func validVersion(version VersionInfo) bool {
|
||||
stopChars := " \t\r\n/"
|
||||
if strings.ContainsAny(version.Name(), stopChars) {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(version.Version(), stopChars) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Convert versions to a string and append the string to the string base.
|
||||
//
|
||||
// Each VersionInfo will be converted to a string in the format of
|
||||
// "product/version", where the "product" is get from the Name() method, while
|
||||
// version is get from the Version() method. Several pieces of verson information
|
||||
// will be concatinated and separated by space.
|
||||
func appendVersions(base string, versions ...VersionInfo) string {
|
||||
if len(versions) == 0 {
|
||||
return base
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if len(base) > 0 {
|
||||
buf.Write([]byte(base))
|
||||
}
|
||||
|
||||
for _, v := range versions {
|
||||
if !validVersion(v) {
|
||||
continue
|
||||
}
|
||||
buf.Write([]byte(v.Name()))
|
||||
buf.Write([]byte("/"))
|
||||
buf.Write([]byte(v.Version()))
|
||||
buf.Write([]byte(" "))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func NewRegistry(root string, authConfig *auth.AuthConfig, baseVersions ...VersionInfo) (r *Registry, err error) {
|
||||
httpTransport := &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
|
@ -553,5 +686,10 @@ func NewRegistry(root string, authConfig *auth.AuthConfig) (r *Registry, err err
|
|||
},
|
||||
}
|
||||
r.client.Jar, err = cookiejar.New(nil)
|
||||
return r, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.baseVersions = baseVersions
|
||||
r.baseVersionsStr = appendVersions("", baseVersions...)
|
||||
return r, nil
|
||||
}
|
||||
|
|
|
@ -167,12 +167,12 @@ func (runtime *Runtime) Register(container *Container) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (runtime *Runtime) LogToDisk(src *utils.WriteBroadcaster, dst string) error {
|
||||
func (runtime *Runtime) LogToDisk(src *utils.WriteBroadcaster, dst, stream string) error {
|
||||
log, err := os.OpenFile(dst, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src.AddWriter(log)
|
||||
src.AddWriter(log, stream)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -26,7 +26,11 @@ const (
|
|||
testDaemonProto = "tcp"
|
||||
)
|
||||
|
||||
var globalRuntime *Runtime
|
||||
var (
|
||||
globalRuntime *Runtime
|
||||
startFds int
|
||||
startGoroutines int
|
||||
)
|
||||
|
||||
func nuke(runtime *Runtime) error {
|
||||
var wg sync.WaitGroup
|
||||
|
@ -81,21 +85,21 @@ func init() {
|
|||
NetworkBridgeIface = unitTestNetworkBridge
|
||||
|
||||
// Make it our Store root
|
||||
runtime, err := NewRuntimeFromDirectory(unitTestStoreBase, false)
|
||||
if err != nil {
|
||||
if runtime, err := NewRuntimeFromDirectory(unitTestStoreBase, false); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
globalRuntime = runtime
|
||||
}
|
||||
globalRuntime = runtime
|
||||
|
||||
// Create the "Server"
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
runtime: globalRuntime,
|
||||
enableCors: false,
|
||||
pullingPool: make(map[string]struct{}),
|
||||
pushingPool: make(map[string]struct{}),
|
||||
}
|
||||
// If the unit test is not found, try to download it.
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageName); err != nil || img.ID != unitTestImageID {
|
||||
if img, err := globalRuntime.repositories.LookupImage(unitTestImageName); err != nil || img.ID != unitTestImageID {
|
||||
// Retrieve the Image
|
||||
if err := srv.ImagePull(unitTestImageName, "", os.Stdout, utils.NewStreamFormatter(false), nil); err != nil {
|
||||
panic(err)
|
||||
|
@ -110,6 +114,8 @@ func init() {
|
|||
|
||||
// Give some time to ListenAndServer to actually start
|
||||
time.Sleep(time.Second)
|
||||
|
||||
startFds, startGoroutines = utils.GetTotalUsedFds(), runtime.NumGoroutine()
|
||||
}
|
||||
|
||||
// FIXME: test that ImagePull(json=true) send correct json output
|
||||
|
@ -247,36 +253,13 @@ func TestGet(t *testing.T) {
|
|||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
container1, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container1, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
|
||||
defer runtime.Destroy(container1)
|
||||
|
||||
container2, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
|
||||
defer runtime.Destroy(container2)
|
||||
|
||||
container3, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container3, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
|
||||
defer runtime.Destroy(container3)
|
||||
|
||||
if runtime.Get(container1.ID) != container1 {
|
||||
|
@ -431,46 +414,14 @@ func TestAllocateUDPPortLocalhost(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRestore(t *testing.T) {
|
||||
|
||||
root, err := ioutil.TempDir("", "docker-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := utils.CopyDirectory(unitTestStoreBase, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runtime1, err := NewRuntimeFromDirectory(root, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
builder := NewBuilder(runtime1)
|
||||
|
||||
runtime1 := mkRuntime(t)
|
||||
defer nuke(runtime1)
|
||||
// Create a container with one instance of docker
|
||||
container1, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime1).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container1, _, _ := mkContainer(runtime1, []string{"_", "ls", "-al"}, t)
|
||||
defer runtime1.Destroy(container1)
|
||||
|
||||
// Create a second container meant to be killed
|
||||
container2, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime1).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2, _, _ := mkContainer(runtime1, []string{"-i", "_", "/bin/cat"}, t)
|
||||
defer runtime1.Destroy(container2)
|
||||
|
||||
// Start the container non blocking
|
||||
|
@ -505,7 +456,7 @@ func TestRestore(t *testing.T) {
|
|||
|
||||
// Here are are simulating a docker restart - that is, reloading all containers
|
||||
// from scratch
|
||||
runtime2, err := NewRuntimeFromDirectory(root, false)
|
||||
runtime2, err := NewRuntimeFromDirectory(runtime1.root, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
300
server.go
300
server.go
|
@ -2,6 +2,7 @@ package docker
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/auth"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (srv *Server) DockerVersion() APIVersion {
|
||||
|
@ -28,11 +30,53 @@ func (srv *Server) DockerVersion() APIVersion {
|
|||
}
|
||||
}
|
||||
|
||||
// simpleVersionInfo is a simple implementation of
|
||||
// the interface VersionInfo, which is used
|
||||
// to provide version information for some product,
|
||||
// component, etc. It stores the product name and the version
|
||||
// in string and returns them on calls to Name() and Version().
|
||||
type simpleVersionInfo struct {
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
func (v *simpleVersionInfo) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *simpleVersionInfo) Version() string {
|
||||
return v.version
|
||||
}
|
||||
|
||||
// versionCheckers() returns version informations of:
|
||||
// docker, go, git-commit (of the docker) and the host's kernel.
|
||||
//
|
||||
// Such information will be used on call to NewRegistry().
|
||||
func (srv *Server) versionInfos() []registry.VersionInfo {
|
||||
v := srv.DockerVersion()
|
||||
ret := make([]registry.VersionInfo, 0, 4)
|
||||
ret = append(ret, &simpleVersionInfo{"docker", v.Version})
|
||||
|
||||
if len(v.GoVersion) > 0 {
|
||||
ret = append(ret, &simpleVersionInfo{"go", v.GoVersion})
|
||||
}
|
||||
if len(v.GitCommit) > 0 {
|
||||
ret = append(ret, &simpleVersionInfo{"git-commit", v.GitCommit})
|
||||
}
|
||||
kernelVersion, err := utils.GetKernelVersion()
|
||||
if err == nil {
|
||||
ret = append(ret, &simpleVersionInfo{"kernel", kernelVersion.String()})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (srv *Server) ContainerKill(name string) error {
|
||||
if container := srv.runtime.Get(name); container != nil {
|
||||
if err := container.Kill(); err != nil {
|
||||
return fmt.Errorf("Error restarting container %s: %s", name, err)
|
||||
return fmt.Errorf("Error killing container %s: %s", name, err)
|
||||
}
|
||||
srv.LogEvent("kill", name)
|
||||
} else {
|
||||
return fmt.Errorf("No such container: %s", name)
|
||||
}
|
||||
|
@ -51,13 +95,14 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
|
|||
if _, err := io.Copy(out, data); err != nil {
|
||||
return err
|
||||
}
|
||||
srv.LogEvent("export", name)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("No such container: %s", name)
|
||||
}
|
||||
|
||||
func (srv *Server) ImagesSearch(term string) ([]APISearch, error) {
|
||||
r, err := registry.NewRegistry(srv.runtime.root, nil)
|
||||
r, err := registry.NewRegistry(srv.runtime.root, nil, srv.versionInfos()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -207,14 +252,29 @@ func (srv *Server) DockerInfo() *APIInfo {
|
|||
} else {
|
||||
imgcount = len(images)
|
||||
}
|
||||
lxcVersion := ""
|
||||
if output, err := exec.Command("lxc-version").CombinedOutput(); err == nil {
|
||||
outputStr := string(output)
|
||||
if len(strings.SplitN(outputStr, ":", 2)) == 2 {
|
||||
lxcVersion = strings.TrimSpace(strings.SplitN(string(output), ":", 2)[1])
|
||||
}
|
||||
}
|
||||
kernelVersion := "<unknown>"
|
||||
if kv, err := utils.GetKernelVersion(); err == nil {
|
||||
kernelVersion = kv.String()
|
||||
}
|
||||
|
||||
return &APIInfo{
|
||||
Containers: len(srv.runtime.List()),
|
||||
Images: imgcount,
|
||||
MemoryLimit: srv.runtime.capabilities.MemoryLimit,
|
||||
SwapLimit: srv.runtime.capabilities.SwapLimit,
|
||||
Debug: os.Getenv("DEBUG") != "",
|
||||
NFd: utils.GetTotalUsedFds(),
|
||||
NGoroutines: runtime.NumGoroutine(),
|
||||
Containers: len(srv.runtime.List()),
|
||||
Images: imgcount,
|
||||
MemoryLimit: srv.runtime.capabilities.MemoryLimit,
|
||||
SwapLimit: srv.runtime.capabilities.SwapLimit,
|
||||
Debug: os.Getenv("DEBUG") != "",
|
||||
NFd: utils.GetTotalUsedFds(),
|
||||
NGoroutines: runtime.NumGoroutine(),
|
||||
LXCVersion: lxcVersion,
|
||||
NEventsListener: len(srv.events),
|
||||
KernelVersion: kernelVersion,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,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 {
|
||||
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 {
|
||||
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") {
|
||||
if i == 0 || len(line) == 0 {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
proc := APITop{}
|
||||
words := []string{}
|
||||
scanner := bufio.NewScanner(strings.NewReader(line))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("Error trying to use lxc-ps")
|
||||
}
|
||||
// no scanner.Text because we skip container id
|
||||
scanner.Scan()
|
||||
proc.PID = scanner.Text()
|
||||
scanner.Scan()
|
||||
proc.Tty = scanner.Text()
|
||||
scanner.Scan()
|
||||
proc.Time = scanner.Text()
|
||||
scanner.Scan()
|
||||
proc.Cmd = scanner.Text()
|
||||
procs = append(procs, proc)
|
||||
for scanner.Scan() {
|
||||
words = append(words, scanner.Text())
|
||||
}
|
||||
if i == 0 {
|
||||
procs.Titles = words
|
||||
} else {
|
||||
procs.Processes = append(procs.Processes, words)
|
||||
}
|
||||
}
|
||||
return procs, nil
|
||||
return &procs, nil
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("No such container: %s", name)
|
||||
|
@ -379,7 +438,7 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgID, endpoin
|
|||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -395,12 +454,6 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, localName
|
|||
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")
|
||||
tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens)
|
||||
if err != nil {
|
||||
|
@ -506,7 +559,7 @@ func (srv *Server) poolRemove(kind, key string) error {
|
|||
}
|
||||
|
||||
func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error {
|
||||
r, err := registry.NewRegistry(srv.runtime.root, authConfig)
|
||||
r, err := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -538,41 +591,6 @@ func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *ut
|
|||
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
|
||||
// Note: we can't use a map as it is not ordered
|
||||
func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgData, error) {
|
||||
|
@ -589,14 +607,10 @@ func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgDat
|
|||
return nil
|
||||
}
|
||||
imageSet[img.ID] = struct{}{}
|
||||
checksum, err := srv.getChecksum(img.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgList = append([]*registry.ImgData{{
|
||||
ID: img.ID,
|
||||
Checksum: checksum,
|
||||
Tag: tag,
|
||||
ID: img.ID,
|
||||
Tag: tag,
|
||||
}}, imgList...)
|
||||
return nil
|
||||
})
|
||||
|
@ -606,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 {
|
||||
out = utils.NewWriteFlusher(out)
|
||||
out.Write(sf.FormatStatus("Processing checksums"))
|
||||
|
||||
imgList, err := srv.getImageList(localRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -630,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))
|
||||
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?
|
||||
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))
|
||||
if err := r.PushRegistryTag(remoteName, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil {
|
||||
|
@ -648,64 +664,45 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, localName
|
|||
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)
|
||||
jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgID, "json"))
|
||||
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))
|
||||
|
||||
// Make sure we have the image's checksum
|
||||
checksum, err := srv.getChecksum(imgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imgData := ®istry.ImgData{
|
||||
ID: imgID,
|
||||
Checksum: checksum,
|
||||
ID: imgID,
|
||||
}
|
||||
|
||||
// Send the json
|
||||
if err := r.PushImageJSONRegistry(imgData, jsonRaw, ep, token); err != nil {
|
||||
if err == registry.ErrAlreadyExists {
|
||||
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
|
||||
var layerData *TempArchive
|
||||
// If the archive exists, use it
|
||||
file, err := os.Open(layerArchivePath(srv.runtime.graph.imageRoot(imgID)))
|
||||
layerData, err := srv.runtime.graph.TempLayerArchive(imgID, Uncompressed, sf, out)
|
||||
if err != nil {
|
||||
if os.IsNotExist(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(),
|
||||
}
|
||||
return "", fmt.Errorf("Failed to generate layer archive: %s", err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
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
|
||||
} 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.
|
||||
|
@ -723,7 +720,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo
|
|||
|
||||
out = utils.NewWriteFlusher(out)
|
||||
img, err := srv.runtime.graph.Get(localName)
|
||||
r, err2 := registry.NewRegistry(srv.runtime.root, authConfig)
|
||||
r, err2 := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
@ -743,7 +740,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo
|
|||
|
||||
var token []string
|
||||
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 nil
|
||||
|
@ -809,6 +806,7 @@ func (srv *Server) ContainerCreate(config *Config) (string, error) {
|
|||
}
|
||||
return "", err
|
||||
}
|
||||
srv.LogEvent("create", container.ShortID())
|
||||
return container.ShortID(), nil
|
||||
}
|
||||
|
||||
|
@ -817,6 +815,7 @@ func (srv *Server) ContainerRestart(name string, t int) error {
|
|||
if err := container.Restart(t); err != nil {
|
||||
return fmt.Errorf("Error restarting container %s: %s", name, err)
|
||||
}
|
||||
srv.LogEvent("restart", name)
|
||||
} else {
|
||||
return fmt.Errorf("No such container: %s", name)
|
||||
}
|
||||
|
@ -836,6 +835,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error {
|
|||
if err := srv.runtime.Destroy(container); err != nil {
|
||||
return fmt.Errorf("Error destroying container %s: %s", name, err)
|
||||
}
|
||||
srv.LogEvent("destroy", name)
|
||||
|
||||
if removeVolume {
|
||||
// Retrieve all volumes from all remaining containers
|
||||
|
@ -902,6 +902,7 @@ func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error {
|
|||
return err
|
||||
}
|
||||
*imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
|
||||
srv.LogEvent("delete", utils.TruncateID(id))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
@ -931,6 +932,9 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro
|
|||
parsedRepo := strings.Split(repoAndTag, ":")[0]
|
||||
if strings.Contains(img.ID, repoName) {
|
||||
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 {
|
||||
// the id belongs to multiple repos, like base:latest and user:test,
|
||||
// in that case return conflict
|
||||
|
@ -945,6 +949,7 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro
|
|||
}
|
||||
if tagDeleted {
|
||||
imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
|
||||
srv.LogEvent("untag", img.ShortID())
|
||||
}
|
||||
if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
|
||||
if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
|
||||
|
@ -1017,6 +1022,7 @@ func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error {
|
|||
if err := container.Start(hostConfig); err != nil {
|
||||
return fmt.Errorf("Error starting container %s: %s", name, err)
|
||||
}
|
||||
srv.LogEvent("start", name)
|
||||
} else {
|
||||
return fmt.Errorf("No such container: %s", name)
|
||||
}
|
||||
|
@ -1028,6 +1034,7 @@ func (srv *Server) ContainerStop(name string, t int) error {
|
|||
if err := container.Stop(t); err != nil {
|
||||
return fmt.Errorf("Error stopping container %s: %s", name, err)
|
||||
}
|
||||
srv.LogEvent("stop", name)
|
||||
} else {
|
||||
return fmt.Errorf("No such container: %s", name)
|
||||
}
|
||||
|
@ -1055,20 +1062,41 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std
|
|||
}
|
||||
//logs
|
||||
if logs {
|
||||
if stdout {
|
||||
cLog, err := container.ReadLog("stdout")
|
||||
if err != nil {
|
||||
utils.Debugf("Error reading logs (stdout): %s", err)
|
||||
} else if _, err := io.Copy(out, cLog); err != nil {
|
||||
utils.Debugf("Error streaming logs (stdout): %s", err)
|
||||
cLog, err := container.ReadLog("json")
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
// Legacy logs
|
||||
utils.Debugf("Old logs format")
|
||||
if stdout {
|
||||
cLog, err := container.ReadLog("stdout")
|
||||
if err != nil {
|
||||
utils.Debugf("Error reading logs (stdout): %s", err)
|
||||
} else if _, err := io.Copy(out, cLog); err != nil {
|
||||
utils.Debugf("Error streaming logs (stdout): %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if stderr {
|
||||
cLog, err := container.ReadLog("stderr")
|
||||
if err != nil {
|
||||
utils.Debugf("Error reading logs (stderr): %s", err)
|
||||
} else if _, err := io.Copy(out, cLog); err != nil {
|
||||
utils.Debugf("Error streaming logs (stderr): %s", err)
|
||||
if stderr {
|
||||
cLog, err := container.ReadLog("stderr")
|
||||
if err != nil {
|
||||
utils.Debugf("Error reading logs (stderr): %s", err)
|
||||
} else if _, err := io.Copy(out, cLog); err != nil {
|
||||
utils.Debugf("Error streaming logs (stderr): %s", err)
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
utils.Debugf("Error reading logs (json): %s", err)
|
||||
} else {
|
||||
dec := json.NewDecoder(cLog)
|
||||
for {
|
||||
var l utils.JSONLog
|
||||
if err := dec.Decode(&l); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
utils.Debugf("Error streaming logs: %s", err)
|
||||
break
|
||||
}
|
||||
if (l.Stream == "stdout" && stdout) || (l.Stream == "stderr" && stderr) {
|
||||
fmt.Fprintf(out, "%s", l.Log)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1140,15 +1168,31 @@ func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (
|
|||
enableCors: enableCors,
|
||||
pullingPool: make(map[string]struct{}),
|
||||
pushingPool: make(map[string]struct{}),
|
||||
events: make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events
|
||||
listeners: make(map[string]chan utils.JSONMessage),
|
||||
}
|
||||
runtime.srv = srv
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func (srv *Server) LogEvent(action, id string) {
|
||||
now := time.Now().Unix()
|
||||
jm := utils.JSONMessage{Status: action, ID: id, Time: now}
|
||||
srv.events = append(srv.events, jm)
|
||||
for _, c := range srv.listeners {
|
||||
select { // non blocking channel
|
||||
case c <- jm:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
sync.Mutex
|
||||
runtime *Runtime
|
||||
enableCors bool
|
||||
pullingPool map[string]struct{}
|
||||
pushingPool map[string]struct{}
|
||||
events []utils.JSONMessage
|
||||
listeners map[string]chan utils.JSONMessage
|
||||
}
|
||||
|
|
147
server_test.go
147
server_test.go
|
@ -1,7 +1,10 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestContainerTagImageDelete(t *testing.T) {
|
||||
|
@ -88,6 +91,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) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
@ -163,3 +187,126 @@ func TestRunWithTooLowMemoryLimit(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLogEvent(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
events: make([]utils.JSONMessage, 0, 64),
|
||||
listeners: make(map[string]chan utils.JSONMessage),
|
||||
}
|
||||
|
||||
srv.LogEvent("fakeaction", "fakeid")
|
||||
|
||||
listener := make(chan utils.JSONMessage)
|
||||
srv.Lock()
|
||||
srv.listeners["test"] = listener
|
||||
srv.Unlock()
|
||||
|
||||
srv.LogEvent("fakeaction2", "fakeid")
|
||||
|
||||
if len(srv.events) != 2 {
|
||||
t.Fatalf("Expected 2 events, found %d", len(srv.events))
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
srv.LogEvent("fakeaction3", "fakeid")
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
srv.LogEvent("fakeaction4", "fakeid")
|
||||
}()
|
||||
|
||||
setTimeout(t, "Listening for events timed out", 2*time.Second, func() {
|
||||
for i := 2; i < 4; i++ {
|
||||
event := <-listener
|
||||
if event != srv.events[i] {
|
||||
t.Fatalf("Event received it different than expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import os
|
|||
from buildbot.buildslave import BuildSlave
|
||||
from buildbot.schedulers.forcesched import ForceScheduler
|
||||
from buildbot.schedulers.basic import SingleBranchScheduler
|
||||
from buildbot.schedulers.timed import Nightly
|
||||
from buildbot.changes import filter
|
||||
from buildbot.config import BuilderConfig
|
||||
from buildbot.process.factory import BuildFactory
|
||||
|
@ -40,12 +41,16 @@ c['db'] = {'db_url':"sqlite:///state.sqlite"}
|
|||
c['slaves'] = [BuildSlave('buildworker', BUILDBOT_PWD)]
|
||||
c['slavePortnum'] = PORT_MASTER
|
||||
|
||||
c['schedulers'] = [ForceScheduler(name='trigger',builderNames=[BUILDER_NAME])]
|
||||
c['schedulers'].append(SingleBranchScheduler(name="all",
|
||||
change_filter=filter.ChangeFilter(branch='master'),treeStableTimer=None,
|
||||
builderNames=[BUILDER_NAME]))
|
||||
# Schedulers
|
||||
c['schedulers'] = [ForceScheduler(name='trigger', builderNames=[BUILDER_NAME,
|
||||
'coverage'])]
|
||||
c['schedulers'] += [SingleBranchScheduler(name="all",
|
||||
change_filter=filter.ChangeFilter(branch='master'), treeStableTimer=None,
|
||||
builderNames=[BUILDER_NAME])]
|
||||
c['schedulers'] += [Nightly(name='daily', branch=None, builderNames=['coverage'],
|
||||
hour=0, minute=30)]
|
||||
|
||||
# Builder
|
||||
# Builders
|
||||
factory = BuildFactory()
|
||||
factory.addStep(ShellCommand(description='Docker',logEnviron=False,usePTY=True,
|
||||
command=["sh", "-c", Interpolate("cd ..; rm -rf build; export GOPATH={0}; "
|
||||
|
@ -53,6 +58,16 @@ factory.addStep(ShellCommand(description='Docker',logEnviron=False,usePTY=True,
|
|||
"go test -v".format(BUILDER_PATH,GITHUB_DOCKER,DOCKER_BUILD_PATH))]))
|
||||
c['builders'] = [BuilderConfig(name=BUILDER_NAME,slavenames=['buildworker'],
|
||||
factory=factory)]
|
||||
# Docker coverage test
|
||||
coverage_cmd = ('GOPATH=`pwd` go get -d github.com/dotcloud/docker\n'
|
||||
'GOPATH=`pwd` go get github.com/axw/gocov/gocov\n'
|
||||
'sudo -E GOPATH=`pwd` ./bin/gocov test github.com/dotcloud/docker | '
|
||||
'./bin/gocov report')
|
||||
factory = BuildFactory()
|
||||
factory.addStep(ShellCommand(description='Coverage',logEnviron=False,usePTY=True,
|
||||
command=coverage_cmd))
|
||||
c['builders'] += [BuilderConfig(name='coverage',slavenames=['buildworker'],
|
||||
factory=factory)]
|
||||
|
||||
# Status
|
||||
authz_cfg = authz.Authz(auth=auth.BasicAuth([(TEST_USER, TEST_PWD)]),
|
||||
|
|
2
utils.go
2
utils.go
|
@ -78,7 +78,7 @@ func MergeConfig(userConf, imageConf *Config) {
|
|||
imageNat, _ := parseNat(imagePortSpec)
|
||||
for _, userPortSpec := range userConf.PortSpecs {
|
||||
userNat, _ := parseNat(userPortSpec)
|
||||
if imageNat.Proto == userNat.Proto && imageNat.Frontend == userNat.Frontend {
|
||||
if imageNat.Proto == userNat.Proto && imageNat.Backend == userNat.Backend {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -248,30 +248,54 @@ func (r *bufReader) Close() error {
|
|||
|
||||
type WriteBroadcaster struct {
|
||||
sync.Mutex
|
||||
writers map[io.WriteCloser]struct{}
|
||||
buf *bytes.Buffer
|
||||
writers map[StreamWriter]bool
|
||||
}
|
||||
|
||||
func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser) {
|
||||
type StreamWriter struct {
|
||||
wc io.WriteCloser
|
||||
stream string
|
||||
}
|
||||
|
||||
func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser, stream string) {
|
||||
w.Lock()
|
||||
w.writers[writer] = struct{}{}
|
||||
sw := StreamWriter{wc: writer, stream: stream}
|
||||
w.writers[sw] = true
|
||||
w.Unlock()
|
||||
}
|
||||
|
||||
// FIXME: Is that function used?
|
||||
// FIXME: This relies on the concrete writer type used having equality operator
|
||||
func (w *WriteBroadcaster) RemoveWriter(writer io.WriteCloser) {
|
||||
w.Lock()
|
||||
delete(w.writers, writer)
|
||||
w.Unlock()
|
||||
type JSONLog struct {
|
||||
Log string `json:"log,omitempty"`
|
||||
Stream string `json:"stream,omitempty"`
|
||||
Created time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
for writer := range w.writers {
|
||||
if n, err := writer.Write(p); err != nil || n != len(p) {
|
||||
w.buf.Write(p)
|
||||
for sw := range w.writers {
|
||||
lp := p
|
||||
if sw.stream != "" {
|
||||
lp = nil
|
||||
for {
|
||||
line, err := w.buf.ReadString('\n')
|
||||
if err != nil {
|
||||
w.buf.Write([]byte(line))
|
||||
break
|
||||
}
|
||||
b, err := json.Marshal(&JSONLog{Log: line, Stream: sw.stream, Created: time.Now()})
|
||||
if err != nil {
|
||||
// On error, evict the writer
|
||||
delete(w.writers, sw)
|
||||
continue
|
||||
}
|
||||
lp = append(lp, b...)
|
||||
}
|
||||
}
|
||||
if n, err := sw.wc.Write(lp); err != nil || n != len(lp) {
|
||||
// On error, evict the writer
|
||||
delete(w.writers, writer)
|
||||
delete(w.writers, sw)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
|
@ -280,15 +304,15 @@ func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
|||
func (w *WriteBroadcaster) CloseWriters() error {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
for writer := range w.writers {
|
||||
writer.Close()
|
||||
for sw := range w.writers {
|
||||
sw.wc.Close()
|
||||
}
|
||||
w.writers = make(map[io.WriteCloser]struct{})
|
||||
w.writers = make(map[StreamWriter]bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWriteBroadcaster() *WriteBroadcaster {
|
||||
return &WriteBroadcaster{writers: make(map[io.WriteCloser]struct{})}
|
||||
return &WriteBroadcaster{writers: make(map[StreamWriter]bool), buf: bytes.NewBuffer(nil)}
|
||||
}
|
||||
|
||||
func GetTotalUsedFds() int {
|
||||
|
@ -587,6 +611,24 @@ type JSONMessage struct {
|
|||
Status string `json:"status,omitempty"`
|
||||
Progress string `json:"progress,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Time int64 `json:"time,omitempty"`
|
||||
}
|
||||
|
||||
func (jm *JSONMessage) Display(out io.Writer) error {
|
||||
if jm.Time != 0 {
|
||||
fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0))
|
||||
}
|
||||
if jm.Progress != "" {
|
||||
fmt.Fprintf(out, "%s %s\r", jm.Status, jm.Progress)
|
||||
} else if jm.Error != "" {
|
||||
return fmt.Errorf(jm.Error)
|
||||
} else if jm.ID != "" {
|
||||
fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s\n", jm.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamFormatter struct {
|
||||
|
|
|
@ -60,9 +60,9 @@ func TestWriteBroadcaster(t *testing.T) {
|
|||
|
||||
// Test 1: Both bufferA and bufferB should contain "foo"
|
||||
bufferA := &dummyWriter{}
|
||||
writer.AddWriter(bufferA)
|
||||
writer.AddWriter(bufferA, "")
|
||||
bufferB := &dummyWriter{}
|
||||
writer.AddWriter(bufferB)
|
||||
writer.AddWriter(bufferB, "")
|
||||
writer.Write([]byte("foo"))
|
||||
|
||||
if bufferA.String() != "foo" {
|
||||
|
@ -76,7 +76,7 @@ func TestWriteBroadcaster(t *testing.T) {
|
|||
// Test2: bufferA and bufferB should contain "foobar",
|
||||
// while bufferC should only contain "bar"
|
||||
bufferC := &dummyWriter{}
|
||||
writer.AddWriter(bufferC)
|
||||
writer.AddWriter(bufferC, "")
|
||||
writer.Write([]byte("bar"))
|
||||
|
||||
if bufferA.String() != "foobar" {
|
||||
|
@ -91,35 +91,22 @@ func TestWriteBroadcaster(t *testing.T) {
|
|||
t.Errorf("Buffer contains %v", bufferC.String())
|
||||
}
|
||||
|
||||
// Test3: Test removal
|
||||
writer.RemoveWriter(bufferB)
|
||||
writer.Write([]byte("42"))
|
||||
if bufferA.String() != "foobar42" {
|
||||
t.Errorf("Buffer contains %v", bufferA.String())
|
||||
}
|
||||
if bufferB.String() != "foobar" {
|
||||
t.Errorf("Buffer contains %v", bufferB.String())
|
||||
}
|
||||
if bufferC.String() != "bar42" {
|
||||
t.Errorf("Buffer contains %v", bufferC.String())
|
||||
}
|
||||
|
||||
// Test4: Test eviction on failure
|
||||
// Test3: Test eviction on failure
|
||||
bufferA.failOnWrite = true
|
||||
writer.Write([]byte("fail"))
|
||||
if bufferA.String() != "foobar42" {
|
||||
if bufferA.String() != "foobar" {
|
||||
t.Errorf("Buffer contains %v", bufferA.String())
|
||||
}
|
||||
if bufferC.String() != "bar42fail" {
|
||||
if bufferC.String() != "barfail" {
|
||||
t.Errorf("Buffer contains %v", bufferC.String())
|
||||
}
|
||||
// Even though we reset the flag, no more writes should go in there
|
||||
bufferA.failOnWrite = false
|
||||
writer.Write([]byte("test"))
|
||||
if bufferA.String() != "foobar42" {
|
||||
if bufferA.String() != "foobar" {
|
||||
t.Errorf("Buffer contains %v", bufferA.String())
|
||||
}
|
||||
if bufferC.String() != "bar42failtest" {
|
||||
if bufferC.String() != "barfailtest" {
|
||||
t.Errorf("Buffer contains %v", bufferC.String())
|
||||
}
|
||||
|
||||
|
@ -141,7 +128,7 @@ func TestRaceWriteBroadcaster(t *testing.T) {
|
|||
writer := NewWriteBroadcaster()
|
||||
c := make(chan bool)
|
||||
go func() {
|
||||
writer.AddWriter(devNullCloser(0))
|
||||
writer.AddWriter(devNullCloser(0), "")
|
||||
c <- true
|
||||
}()
|
||||
writer.Write([]byte("hello"))
|
||||
|
|
|
@ -84,18 +84,25 @@ func readFile(src string, t *testing.T) (content string) {
|
|||
}
|
||||
|
||||
// Create a test container from the given runtime `r` and run arguments `args`.
|
||||
// The image name (eg. the XXX in []string{"-i", "-t", "XXX", "bash"}, is dynamically replaced by the current test image.
|
||||
// If the image name is "_", (eg. []string{"-i", "-t", "_", "bash"}, it is
|
||||
// dynamically replaced by the current test image.
|
||||
// The caller is responsible for destroying the container.
|
||||
// Call t.Fatal() at the first error.
|
||||
func mkContainer(r *Runtime, args []string, t *testing.T) (*Container, *HostConfig, error) {
|
||||
config, hostConfig, _, err := ParseRun(args, nil)
|
||||
defer func() {
|
||||
if err != nil && t != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
config.Image = GetTestImage(r).ID
|
||||
if config.Image == "_" {
|
||||
config.Image = GetTestImage(r).ID
|
||||
}
|
||||
c, err := NewBuilder(r).Create(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
return c, hostConfig, nil
|
||||
|
@ -148,7 +155,7 @@ func TestMergeConfig(t *testing.T) {
|
|||
volumesUser["/test3"] = struct{}{}
|
||||
configUser := &Config{
|
||||
Dns: []string{"3.3.3.3"},
|
||||
PortSpecs: []string{"2222:3333", "3333:3333"},
|
||||
PortSpecs: []string{"3333:2222", "3333:3333"},
|
||||
Env: []string{"VAR2=3", "VAR3=3"},
|
||||
Volumes: volumesUser,
|
||||
}
|
||||
|
@ -165,11 +172,11 @@ func TestMergeConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
if len(configUser.PortSpecs) != 3 {
|
||||
t.Fatalf("Expected 3 portSpecs, 1111:1111, 2222:3333 and 3333:3333, found %d", len(configUser.PortSpecs))
|
||||
t.Fatalf("Expected 3 portSpecs, 1111:1111, 3333:2222 and 3333:3333, found %d", len(configUser.PortSpecs))
|
||||
}
|
||||
for _, portSpecs := range configUser.PortSpecs {
|
||||
if portSpecs != "1111:1111" && portSpecs != "2222:3333" && portSpecs != "3333:3333" {
|
||||
t.Fatalf("Expected 1111:1111 or 2222:3333 or 3333:3333, found %s", portSpecs)
|
||||
if portSpecs != "1111:1111" && portSpecs != "3333:2222" && portSpecs != "3333:3333" {
|
||||
t.Fatalf("Expected 1111:1111 or 3333:2222 or 3333:3333, found %s", portSpecs)
|
||||
}
|
||||
}
|
||||
if len(configUser.Env) != 3 {
|
||||
|
@ -190,3 +197,45 @@ func TestMergeConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigPublicPortNotHonored(t *testing.T) {
|
||||
volumesImage := make(map[string]struct{})
|
||||
volumesImage["/test1"] = struct{}{}
|
||||
volumesImage["/test2"] = struct{}{}
|
||||
configImage := &Config{
|
||||
Dns: []string{"1.1.1.1", "2.2.2.2"},
|
||||
PortSpecs: []string{"1111", "2222"},
|
||||
Env: []string{"VAR1=1", "VAR2=2"},
|
||||
Volumes: volumesImage,
|
||||
}
|
||||
|
||||
volumesUser := make(map[string]struct{})
|
||||
volumesUser["/test3"] = struct{}{}
|
||||
configUser := &Config{
|
||||
Dns: []string{"3.3.3.3"},
|
||||
PortSpecs: []string{"1111:3333"},
|
||||
Env: []string{"VAR2=3", "VAR3=3"},
|
||||
Volumes: volumesUser,
|
||||
}
|
||||
|
||||
MergeConfig(configUser, configImage)
|
||||
|
||||
contains := func(a []string, expect string) bool {
|
||||
for _, p := range a {
|
||||
if p == expect {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if !contains(configUser.PortSpecs, "2222") {
|
||||
t.Logf("Expected '2222' Ports: %v", configUser.PortSpecs)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if !contains(configUser.PortSpecs, "1111:3333") {
|
||||
t.Logf("Expected '1111:3333' Ports: %v", configUser.PortSpecs)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestFinal(t *testing.T) {
|
||||
cleanup(globalRuntime)
|
||||
func displayFdGoroutines(t *testing.T) {
|
||||
t.Logf("Fds: %d, Goroutines: %d", utils.GetTotalUsedFds(), runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
func TestFinal(t *testing.T) {
|
||||
cleanup(globalRuntime)
|
||||
t.Logf("Start Fds: %d, Start Goroutines: %d", startFds, startGoroutines)
|
||||
displayFdGoroutines(t)
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче