diff --git a/daemon/container.go b/daemon/container.go index 6f63d565f2..c06fd2c074 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -654,9 +654,12 @@ func (container *Container) Kill() error { // 2. Wait for the process to die, in last resort, try to kill the process directly if err := container.WaitTimeout(10 * time.Second); err != nil { - log.Printf("Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL", utils.TruncateID(container.ID)) - if err := syscall.Kill(container.State.Pid, 9); err != nil { - return err + // Ensure that we don't kill ourselves + if pid := container.State.Pid; pid != 0 { + log.Printf("Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL", utils.TruncateID(container.ID)) + if err := syscall.Kill(pid, 9); err != nil { + return err + } } } diff --git a/daemon/execdriver/lxc/driver.go b/daemon/execdriver/lxc/driver.go index 1ebb73e807..1232d608a3 100644 --- a/daemon/execdriver/lxc/driver.go +++ b/daemon/execdriver/lxc/driver.go @@ -59,9 +59,10 @@ func init() { } type driver struct { - root string // root path for the driver to use - apparmor bool - sharedRoot bool + root string // root path for the driver to use + apparmor bool + sharedRoot bool + restrictionPath string } func NewDriver(root string, apparmor bool) (*driver, error) { @@ -69,10 +70,15 @@ func NewDriver(root string, apparmor bool) (*driver, error) { if err := linkLxcStart(root); err != nil { return nil, err } + restrictionPath := filepath.Join(root, "empty") + if err := os.MkdirAll(restrictionPath, 0700); err != nil { + return nil, err + } return &driver{ - apparmor: apparmor, - root: root, - sharedRoot: rootIsShared(), + apparmor: apparmor, + root: root, + sharedRoot: rootIsShared(), + restrictionPath: restrictionPath, }, nil } @@ -403,14 +409,16 @@ func (d *driver) generateLXCConfig(c *execdriver.Command) (string, error) { if err := LxcTemplateCompiled.Execute(fo, struct { *execdriver.Command - AppArmor bool - ProcessLabel string - MountLabel string + AppArmor bool + ProcessLabel string + MountLabel string + RestrictionSource string }{ - Command: c, - AppArmor: d.apparmor, - ProcessLabel: process, - MountLabel: mount, + Command: c, + AppArmor: d.apparmor, + ProcessLabel: process, + MountLabel: mount, + RestrictionSource: d.restrictionPath, }); err != nil { return "", err } diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index f4cb3d19eb..bc94e7a19d 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -88,7 +88,9 @@ lxc.mount.entry = proc {{escapeFstabSpaces $ROOTFS}}/proc proc nosuid,nodev,noex # WARNING: sysfs is a known attack vector and should probably be disabled # if your userspace allows it. eg. see http://bit.ly/T9CkqJ +{{if .Privileged}} lxc.mount.entry = sysfs {{escapeFstabSpaces $ROOTFS}}/sys sysfs nosuid,nodev,noexec 0 0 +{{end}} {{if .Tty}} lxc.mount.entry = {{.Console}} {{escapeFstabSpaces $ROOTFS}}/dev/console none bind,rw 0 0 @@ -109,8 +111,15 @@ lxc.mount.entry = {{$value.Source}} {{escapeFstabSpaces $ROOTFS}}/{{escapeFstabS {{if .AppArmor}} lxc.aa_profile = unconfined {{else}} -#lxc.aa_profile = unconfined +# not unconfined {{end}} +{{else}} +# restrict access to proc +lxc.mount.entry = {{.RestrictionSource}} {{escapeFstabSpaces $ROOTFS}}/proc/sys none bind,ro 0 0 +lxc.mount.entry = {{.RestrictionSource}} {{escapeFstabSpaces $ROOTFS}}/proc/irq none bind,ro 0 0 +lxc.mount.entry = {{.RestrictionSource}} {{escapeFstabSpaces $ROOTFS}}/proc/acpi none bind,ro 0 0 +lxc.mount.entry = {{escapeFstabSpaces $ROOTFS}}/dev/null {{escapeFstabSpaces $ROOTFS}}/proc/sysrq-trigger none bind,ro 0 0 +lxc.mount.entry = {{escapeFstabSpaces $ROOTFS}}/dev/null {{escapeFstabSpaces $ROOTFS}}/proc/kcore none bind,ro 0 0 {{end}} # limits diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index 334a97ad4b..f724ef67e6 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -25,6 +25,7 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Container container.Cgroups.Name = c.ID // check to see if we are running in ramdisk to disable pivot root container.NoPivotRoot = os.Getenv("DOCKER_RAMDISK") != "" + container.Context["restriction_path"] = d.restrictionPath if err := d.createNetwork(container, c); err != nil { return nil, err @@ -33,6 +34,8 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Container if err := d.setPrivileged(container); err != nil { return nil, err } + } else { + container.Mounts = append(container.Mounts, libcontainer.Mount{Type: "devtmpfs"}) } if err := d.setupCgroups(container, c); err != nil { return nil, err @@ -81,6 +84,11 @@ func (d *driver) setPrivileged(container *libcontainer.Container) error { c.Enabled = true } container.Cgroups.DeviceAccess = true + + // add sysfs as a mount for privileged containers + container.Mounts = append(container.Mounts, libcontainer.Mount{Type: "sysfs"}) + delete(container.Context, "restriction_path") + if apparmor.IsEnabled() { container.Context["apparmor_profile"] = "unconfined" } @@ -99,7 +107,13 @@ func (d *driver) setupCgroups(container *libcontainer.Container, c *execdriver.C func (d *driver) setupMounts(container *libcontainer.Container, c *execdriver.Command) error { for _, m := range c.Mounts { - container.Mounts = append(container.Mounts, libcontainer.Mount{m.Source, m.Destination, m.Writable, m.Private}) + container.Mounts = append(container.Mounts, libcontainer.Mount{ + Type: "bind", + Source: m.Source, + Destination: m.Destination, + Writable: m.Writable, + Private: m.Private, + }) } return nil } diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index ab82cdcc65..8b374d9938 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -23,7 +23,7 @@ import ( const ( DriverName = "native" - Version = "0.1" + Version = "0.2" BackupApparmorProfilePath = "apparmor/docker.back" // relative to docker root ) @@ -62,6 +62,7 @@ type driver struct { root string initPath string activeContainers map[string]*exec.Cmd + restrictionPath string } func NewDriver(root, initPath string) (*driver, error) { @@ -72,8 +73,14 @@ func NewDriver(root, initPath string) (*driver, error) { if err := apparmor.InstallDefaultProfile(filepath.Join(root, "../..", BackupApparmorProfilePath)); err != nil { return nil, err } + restrictionPath := filepath.Join(root, "empty") + if err := os.MkdirAll(restrictionPath, 0700); err != nil { + return nil, err + } + return &driver{ root: root, + restrictionPath: restrictionPath, initPath: initPath, activeContainers: make(map[string]*exec.Cmd), }, nil diff --git a/daemon/volumes.go b/daemon/volumes.go index 4a5c4475b7..19bb7cab3f 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -246,22 +246,22 @@ func createVolumes(container *Container) error { if err := archive.CopyWithTar(rootVolPath, srcPath); err != nil { return err } + } + } - var stat syscall.Stat_t - if err := syscall.Stat(rootVolPath, &stat); err != nil { - return err - } - var srcStat syscall.Stat_t - if err := syscall.Stat(srcPath, &srcStat); err != nil { - return err - } - // Change the source volume's ownership if it differs from the root - // files that were just copied - if stat.Uid != srcStat.Uid || stat.Gid != srcStat.Gid { - if err := os.Chown(srcPath, int(stat.Uid), int(stat.Gid)); err != nil { - return err - } - } + var stat syscall.Stat_t + if err := syscall.Stat(rootVolPath, &stat); err != nil { + return err + } + var srcStat syscall.Stat_t + if err := syscall.Stat(srcPath, &srcStat); err != nil { + return err + } + // Change the source volume's ownership if it differs from the root + // files that were just copied + if stat.Uid != srcStat.Uid || stat.Gid != srcStat.Gid { + if err := os.Chown(srcPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err } } } diff --git a/docs/README.md b/docs/README.md index a113cb9edd..bbc741d593 100755 --- a/docs/README.md +++ b/docs/README.md @@ -4,74 +4,70 @@ Docker Documentation Overview -------- -The source for Docker documentation is here under ``sources/`` and uses +The source for Docker documentation is here under `sources/` and uses extended Markdown, as implemented by [mkdocs](http://mkdocs.org). -The HTML files are built and hosted on https://docs.docker.io, and update -automatically after each change to the master or release branch of the -[docker files on GitHub](https://github.com/dotcloud/docker) thanks to -post-commit hooks. The "release" branch maps to the "latest" -documentation and the "master" (unreleased development) branch maps to the "master" -documentation. +The HTML files are built and hosted on `https://docs.docker.io`, and +update automatically after each change to the master or release branch +of [Docker on GitHub](https://github.com/dotcloud/docker) +thanks to post-commit hooks. The "docs" branch maps to the "latest" +documentation and the "master" (unreleased development) branch maps to +the "master" documentation. ## Branches -**There are two branches related to editing docs**: ``master`` and a -``docs`` branch. You should always edit -docs on a local branch of the ``master`` branch, and send a PR against ``master``. -That way your fixes -will automatically get included in later releases, and docs maintainers -can easily cherry-pick your changes into the ``docs`` release branch. -In the rare case where your change is not forward-compatible, -you may need to base your changes on the ``docs`` branch. +**There are two branches related to editing docs**: `master` and a +`docs` branch. You should always edit documentation on a local branch +of the `master` branch, and send a PR against `master`. -Now that we have a ``docs`` branch, we can keep the [http://docs.docker.io](http://docs.docker.io) docs -up to date with any bugs found between ``docker`` code releases. +That way your fixes will automatically get included in later releases, +and docs maintainers can easily cherry-pick your changes into the +`docs` release branch. In the rare case where your change is not +forward-compatible, you may need to base your changes on the `docs` +branch. -**Warning**: When *reading* the docs, the [http://beta-docs.docker.io](http://beta-docs.docker.io) documentation may -include features not yet part of any official docker -release. The ``beta-docs`` site should be used only for understanding -bleeding-edge development and ``docs.docker.io`` (which points to the ``docs`` -branch``) should be used for the latest official release. +Also, now that we have a `docs` branch, we can keep the +[http://docs.docker.io](http://docs.docker.io) docs up to date with any +bugs found between `docker` code releases. + +**Warning**: When *reading* the docs, the +[http://beta-docs.docker.io](http://beta-docs.docker.io) documentation +may include features not yet part of any official docker release. The +`beta-docs` site should be used only for understanding bleeding-edge +development and `docs.docker.io` (which points to the `docs` +branch`) should be used for the latest official release. Getting Started --------------- -Docker documentation builds are done in a docker container, which installs all -the required tools, adds the local ``docs/`` directory and builds the HTML -docs. It then starts a HTTP server on port 8000 so that you can connect -and see your changes. +Docker documentation builds are done in a Docker container, which +installs all the required tools, adds the local `docs/` directory and +builds the HTML docs. It then starts a HTTP server on port 8000 so that +you can connect and see your changes. -In the ``docker`` source directory, run: - ```make docs``` +In the root of the `docker` source directory: -If you have any issues you need to debug, you can use ``make docs-shell`` and -then run ``mkdocs serve`` + cd docker + +Run: + + make docs + +If you have any issues you need to debug, you can use `make docs-shell` and +then run `mkdocs serve` # Contributing -## Normal Case: - * Follow the contribution guidelines ([see - ``../CONTRIBUTING.md``](../CONTRIBUTING.md)). + `../CONTRIBUTING.md`](../CONTRIBUTING.md)). * [Remember to sign your work!](../CONTRIBUTING.md#sign-your-work) -* Work in your own fork of the code, we accept pull requests. -* Change the ``.md`` files with your favorite editor -- try to keep the - lines short (80 chars) and respect Markdown conventions. -* Run ``make clean docs`` to clean up old files and generate new ones, - or just ``make docs`` to update after small changes. -* Your static website can now be found in the ``_build`` directory. -* To preview what you have generated run ``make server`` and open - http://localhost:8000/ in your favorite browser. - -``make clean docs`` must complete without any warnings or errors. Working using GitHub's file editor ---------------------------------- Alternatively, for small changes and typos you might want to use GitHub's built in file editor. It allows you to preview your changes -right online (though there can be some differences between GitHub +right on-line (though there can be some differences between GitHub Markdown and mkdocs Markdown). Just be careful not to create many commits. And you must still [sign your work!](../CONTRIBUTING.md#sign-your-work) @@ -79,26 +75,24 @@ Images ------ When you need to add images, try to make them as small as possible -(e.g. as gif). Usually images should go in the same directory as the -.md file which references them, or in a subdirectory if one already +(e.g. as gifs). Usually images should go in the same directory as the +`.md` file which references them, or in a subdirectory if one already exists. Publishing Documentation ------------------------ -To publish a copy of the documentation you need a ``docs/awsconfig`` -file containing AWS settings to deploy to. The release script will +To publish a copy of the documentation you need a `docs/awsconfig` +file containing AWS settings to deploy to. The release script will create an s3 if needed, and will then push the files to it. -``` -[profile dowideit-docs] -aws_access_key_id = IHOIUAHSIDH234rwf.... -aws_secret_access_key = OIUYSADJHLKUHQWIUHE...... -region = ap-southeast-2 -``` + [profile dowideit-docs] + aws_access_key_id = IHOIUAHSIDH234rwf.... + aws_secret_access_key = OIUYSADJHLKUHQWIUHE...... + region = ap-southeast-2 -The ``profile`` name must be the same as the name of the bucket you are -deploying to - which you call from the docker directory: +The `profile` name must be the same as the name of the bucket you are +deploying to - which you call from the `docker` directory: -``make AWS_S3_BUCKET=dowideit-docs docs-release`` + make AWS_S3_BUCKET=dowideit-docs docs-release diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3538642717..0b526e016b 100755 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -106,6 +106,8 @@ pages: - ['reference/api/docker_remote_api_v1.10.md', 'Reference', 'Docker Remote API v1.10'] - ['reference/api/docker_remote_api_v1.9.md', 'Reference', 'Docker Remote API v1.9'] - ['reference/api/remote_api_client_libraries.md', 'Reference', 'Docker Remote API Client Libraries'] +- ['reference/api/docker_io_oauth_api.md', 'Reference', 'Docker IO OAuth API'] +- ['reference/api/docker_io_accounts_api.md', 'Reference', 'Docker IO Accounts API'] # Contribute: - ['contributing/index.md', '**HIDDEN**'] diff --git a/docs/sources/index/accounts.md b/docs/sources/index/accounts.md index c3138b61da..54d015ac2a 100644 --- a/docs/sources/index/accounts.md +++ b/docs/sources/index/accounts.md @@ -14,7 +14,7 @@ to need a [Docker IO](https://www.docker.io) account. ### Registration for a Docker IO Account You can get a Docker IO account by [signing up for one here]( -https://index.docker.io/account/signup/). A valid email address is required to +https://www.docker.io/account/signup/). A valid email address is required to register, which you will need to verify for account activation. ### Email activation process @@ -22,7 +22,7 @@ register, which you will need to verify for account activation. You need to have at least one verified email address to be able to use your Docker IO account. If you can't find the validation email, you can request another by visiting the [Resend Email Confirmation]( -https://index.docker.io/account/resend-email-confirmation/) page. +https://www.docker.io/account/resend-email-confirmation/) page. ### Password reset process diff --git a/docs/sources/installation/ubuntulinux.md b/docs/sources/installation/ubuntulinux.md index 40dc541b6a..04173cf917 100644 --- a/docs/sources/installation/ubuntulinux.md +++ b/docs/sources/installation/ubuntulinux.md @@ -4,10 +4,6 @@ page_keywords: Docker, Docker documentation, requirements, virtualbox, vagrant, # Ubuntu -> **Warning**: -> These instructions have changed for 0.6. If you are upgrading from an -> earlier version, you will need to follow them again. - > **Note**: > Docker is still under heavy development! We don't recommend using it in > production yet, but we're getting closer with each release. Please see @@ -16,6 +12,7 @@ page_keywords: Docker, Docker documentation, requirements, virtualbox, vagrant, Docker is supported on the following versions of Ubuntu: + - [*Ubuntu Trusty 14.04 (LTS) (64-bit)*](#ubuntu-trusty-1404-lts-64-bit) - [*Ubuntu Precise 12.04 (LTS) (64-bit)*](#ubuntu-precise-1204-lts-64-bit) - [*Ubuntu Raring 13.04 and Saucy 13.10 (64 bit)*](#ubuntu-raring-1304-and-saucy-1310-64-bit) @@ -23,6 +20,30 @@ Docker is supported on the following versions of Ubuntu: Please read [*Docker and UFW*](#docker-and-ufw), if you plan to use [UFW (Uncomplicated Firewall)](https://help.ubuntu.com/community/UFW) +## Ubuntu Trusty 14.04 (LTS) (64-bit) + +Ubuntu Trusty comes with a 3.13.0 Linux kernel, and a `docker.io` package which +installs all its prerequisites from Ubuntu's repository. + +> **Note**: +> Ubuntu (and Debian) contain a much older KDE3/GNOME2 package called ``docker``, so the +> package and the executable are called ``docker.io``. + +### Installation + +To install the latest Ubuntu package (may not be the latest Docker release): + + sudo apt-get update + sudo apt-get install docker.io + sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker + +To verify that everything has worked as expected: + + sudo docker run -i -t ubuntu /bin/bash + +Which should download the `ubuntu` image, and then start `bash` in a container. + + ## Ubuntu Precise 12.04 (LTS) (64-bit) This installation path should work at all times. diff --git a/docs/sources/introduction/understanding-docker.md b/docs/sources/introduction/understanding-docker.md index 1c979d5810..5920da5bca 100644 --- a/docs/sources/introduction/understanding-docker.md +++ b/docs/sources/introduction/understanding-docker.md @@ -87,14 +87,14 @@ to you. *Docker is made for humans.* It's easy to get started and easy to build and deploy applications with -Docker: or as we say "*dockerise*" them! As much of Docker as possible +Docker: or as we say "*dockerize*" them! As much of Docker as possible uses plain English for commands and tries to be as lightweight and transparent as possible. We want to get out of the way so you can build and deploy your applications. ### Docker is Portable -*Dockerise And Go!* +*Dockerize And Go!* Docker containers are highly portable. Docker provides a standard container format to hold your applications: diff --git a/docs/sources/reference/api/archive/docker_remote_api_v1.7.rst b/docs/sources/reference/api/archive/docker_remote_api_v1.7.rst index 1bafaddfc5..7a4f688d8f 100644 --- a/docs/sources/reference/api/archive/docker_remote_api_v1.7.rst +++ b/docs/sources/reference/api/archive/docker_remote_api_v1.7.rst @@ -454,6 +454,7 @@ Kill a container HTTP/1.1 204 OK + :query signal: Signal to send to the container (integer). When not set, SIGKILL is assumed and the call will waits for the container to exit. :statuscode 204: no error :statuscode 404: no such container :statuscode 500: server error diff --git a/docs/sources/reference/api/archive/docker_remote_api_v1.8.rst b/docs/sources/reference/api/archive/docker_remote_api_v1.8.rst index 16492dde76..4f1b266bb6 100644 --- a/docs/sources/reference/api/archive/docker_remote_api_v1.8.rst +++ b/docs/sources/reference/api/archive/docker_remote_api_v1.8.rst @@ -482,6 +482,7 @@ Kill a container HTTP/1.1 204 OK + :query signal: Signal to send to the container (integer). When not set, SIGKILL is assumed and the call will waits for the container to exit. :statuscode 204: no error :statuscode 404: no such container :statuscode 500: server error diff --git a/docs/sources/reference/api/docker_io_accounts_api.md b/docs/sources/reference/api/docker_io_accounts_api.md index 8186e306f8..b9f76ba92c 100644 --- a/docs/sources/reference/api/docker_io_accounts_api.md +++ b/docs/sources/reference/api/docker_io_accounts_api.md @@ -237,78 +237,7 @@ automatically sent. "primary": false } -### 1.5 Update an email address for a user - -`PATCH /api/v1.1/users/:username/emails/` - -Update an email address for the specified user to either verify an -email address or set it as the primary email for the user. You -cannot use this endpoint to un-verify an email address. You cannot -use this endpoint to unset the primary email, only set another as -the primary. - - Parameters: - - - **username** – username of the user whose email info is being - updated. - - Json Parameters: - -   - - - **email** (*string*) – the email address to be updated. - - **verified** (*boolean*) – (optional) whether the email address - is verified, must be `true` or absent. - - **primary** (*boolean*) – (optional) whether to set the email - address as the primary email, must be `true` - or absent. - - Request Headers: - -   - - - **Authorization** – required authentication credentials of - either type HTTP Basic or OAuth Bearer Token. - - **Content-Type** – MIME Type of post data. JSON, url-encoded - form data, etc. - - Status Codes: - - - **200** – success, user's email updated. - - **400** – data validation error. - - **401** – authentication error. - - **403** – permission error, authenticated user must be the user - whose data is being updated, OAuth access tokens must have - `email_write` scope. - - **404** – the specified username or email address does not - exist. - - **Example request**: - - Once you have independently verified an email address. - - PATCH /api/v1.1/users/janedoe/emails/ HTTP/1.1 - Host: www.docker.io - Accept: application/json - Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= - - { - "email": "jane.doe+other@example.com", - "verified": true, - } - - **Example response**: - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "email": "jane.doe+other@example.com", - "verified": true, - "primary": false - } - -### 1.6 Delete email address for a user +### 1.5 Delete email address for a user `DELETE /api/v1.1/users/:username/emails/` diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 3c58b1b990..a6aafbeee8 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -127,83 +127,83 @@ entry for each repo/tag on an image, each image is only represented once, with a nested attribute indicating the repo/tags that apply to that image. - Instead of: +Instead of: - HTTP/1.1 200 OK - Content-Type: application/json + HTTP/1.1 200 OK + Content-Type: application/json - [ - { - "VirtualSize": 131506275, - "Size": 131506275, - "Created": 1365714795, - "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", - "Tag": "12.04", - "Repository": "ubuntu" - }, - { - "VirtualSize": 131506275, - "Size": 131506275, - "Created": 1365714795, - "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", - "Tag": "latest", - "Repository": "ubuntu" - }, - { - "VirtualSize": 131506275, - "Size": 131506275, - "Created": 1365714795, - "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", - "Tag": "precise", - "Repository": "ubuntu" - }, - { - "VirtualSize": 180116135, - "Size": 24653, - "Created": 1364102658, - "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Tag": "12.10", - "Repository": "ubuntu" - }, - { - "VirtualSize": 180116135, - "Size": 24653, - "Created": 1364102658, - "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Tag": "quantal", - "Repository": "ubuntu" - } - ] + [ + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "12.04", + "Repository": "ubuntu" + }, + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "latest", + "Repository": "ubuntu" + }, + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "precise", + "Repository": "ubuntu" + }, + { + "VirtualSize": 180116135, + "Size": 24653, + "Created": 1364102658, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Tag": "12.10", + "Repository": "ubuntu" + }, + { + "VirtualSize": 180116135, + "Size": 24653, + "Created": 1364102658, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Tag": "quantal", + "Repository": "ubuntu" + } + ] - The returned json looks like this: +The returned json looks like this: - HTTP/1.1 200 OK - Content-Type: application/json + HTTP/1.1 200 OK + Content-Type: application/json - [ - { - "RepoTags": [ - "ubuntu:12.04", - "ubuntu:precise", - "ubuntu:latest" - ], - "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", - "Created": 1365714795, - "Size": 131506275, - "VirtualSize": 131506275 - }, - { - "RepoTags": [ - "ubuntu:12.10", - "ubuntu:quantal" - ], - "ParentId": "27cf784147099545", - "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Created": 1364102658, - "Size": 24653, - "VirtualSize": 180116135 - } - ] + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] `GET /images/viz` diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.md b/docs/sources/reference/api/docker_remote_api_v1.10.md index c07f96f384..749ff8e383 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.md +++ b/docs/sources/reference/api/docker_remote_api_v1.10.md @@ -459,6 +459,11 @@ Kill the container `id` HTTP/1.1 204 OK + Query Parameters + + - **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + Status Codes: - **204** – no error diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.rst b/docs/sources/reference/api/docker_remote_api_v1.10.rst index 83e2c3c15b..8635ec4826 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.rst +++ b/docs/sources/reference/api/docker_remote_api_v1.10.rst @@ -466,6 +466,7 @@ Kill a container HTTP/1.1 204 OK + :query signal: Signal to send to the container, must be integer or string (i.e. SIGINT). When not set, SIGKILL is assumed and the call will waits for the container to exit. :statuscode 204: no error :statuscode 404: no such container :statuscode 500: server error diff --git a/docs/sources/reference/api/docker_remote_api_v1.11.md b/docs/sources/reference/api/docker_remote_api_v1.11.md index 5e3fdcb0a8..baabade455 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.11.md +++ b/docs/sources/reference/api/docker_remote_api_v1.11.md @@ -461,6 +461,11 @@ Kill the container `id` HTTP/1.1 204 OK + Query Parameters + + - **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + Status Codes: - **204** – no error diff --git a/docs/sources/reference/api/docker_remote_api_v1.11.rst b/docs/sources/reference/api/docker_remote_api_v1.11.rst index 556491c49a..d66b4b1410 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.11.rst +++ b/docs/sources/reference/api/docker_remote_api_v1.11.rst @@ -468,6 +468,7 @@ Kill a container HTTP/1.1 204 OK + :query signal: Signal to send to the container, must be integer or string (i.e. SIGINT). When not set, SIGKILL is assumed and the call will waits for the container to exit. :statuscode 204: no error :statuscode 404: no such container :statuscode 500: server error diff --git a/docs/sources/reference/api/docker_remote_api_v1.9.md b/docs/sources/reference/api/docker_remote_api_v1.9.md index 74e85a7ee6..1fe154a3da 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.9.md +++ b/docs/sources/reference/api/docker_remote_api_v1.9.md @@ -482,6 +482,11 @@ Kill the container `id` HTTP/1.1 204 OK + Query Parameters + + - **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + Status Codes: - **204** – no error diff --git a/docs/sources/reference/api/docker_remote_api_v1.9.rst b/docs/sources/reference/api/docker_remote_api_v1.9.rst index 4bbffcbd36..db0e3bfdae 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.9.rst +++ b/docs/sources/reference/api/docker_remote_api_v1.9.rst @@ -480,6 +480,7 @@ Kill a container HTTP/1.1 204 OK + :query signal: Signal to send to the container, must be integer or string (i.e. SIGINT). When not set, SIGKILL is assumed and the call will waits for the container to exit. :statuscode 204: no error :statuscode 404: no such container :statuscode 500: server error diff --git a/engine/engine.go b/engine/engine.go index aaf5c1f595..dc1984ccb5 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -43,6 +43,7 @@ func unregister(name string) { // containers by executing *jobs*. type Engine struct { handlers map[string]Handler + catchall Handler hack Hack // data for temporary hackery (see hack.go) id string Stdout io.Writer @@ -60,6 +61,10 @@ func (eng *Engine) Register(name string, handler Handler) error { return nil } +func (eng *Engine) RegisterCatchall(catchall Handler) { + eng.catchall = catchall +} + // New initializes a new engine. func New() *Engine { eng := &Engine{ @@ -113,9 +118,13 @@ func (eng *Engine) Job(name string, args ...string) *Job { if eng.Logging { job.Stderr.Add(utils.NopWriteCloser(eng.Stderr)) } - handler, exists := eng.handlers[name] - if exists { - job.handler = handler + if eng.catchall != nil { + job.handler = eng.catchall + } else { + handler, exists := eng.handlers[name] + if exists { + job.handler = handler + } } return job } diff --git a/engine/env.go b/engine/env.go index c43a5ec971..f96795f48c 100644 --- a/engine/env.go +++ b/engine/env.go @@ -36,6 +36,13 @@ func (env *Env) Exists(key string) bool { return exists } +// Len returns the number of keys in the environment. +// Note that len(env) might be different from env.Len(), +// because the same key might be set multiple times. +func (env *Env) Len() int { + return len(env.Map()) +} + func (env *Env) Init(src *Env) { (*env) = make([]string, 0, len(*src)) for _, val := range *src { diff --git a/engine/env_test.go b/engine/env_test.go index c7079ff942..0c66cea04e 100644 --- a/engine/env_test.go +++ b/engine/env_test.go @@ -4,6 +4,34 @@ import ( "testing" ) +func TestEnvLenZero(t *testing.T) { + env := &Env{} + if env.Len() != 0 { + t.Fatalf("%d", env.Len()) + } +} + +func TestEnvLenNotZero(t *testing.T) { + env := &Env{} + env.Set("foo", "bar") + env.Set("ga", "bu") + if env.Len() != 2 { + t.Fatalf("%d", env.Len()) + } +} + +func TestEnvLenDup(t *testing.T) { + env := &Env{ + "foo=bar", + "foo=baz", + "a=b", + } + // len(env) != env.Len() + if env.Len() != 2 { + t.Fatalf("%d", env.Len()) + } +} + func TestNewJob(t *testing.T) { job := mkJob(t, "dummy", "--level=awesome") if job.Name != "dummy" { diff --git a/engine/job.go b/engine/job.go index 50d64011f9..b56155ac1c 100644 --- a/engine/job.go +++ b/engine/job.go @@ -208,3 +208,7 @@ func (job *Job) Error(err error) Status { fmt.Fprintf(job.Stderr, "%s\n", err) return StatusErr } + +func (job *Job) StatusCode() int { + return int(job.status) +} diff --git a/engine/remote.go b/engine/remote.go new file mode 100644 index 0000000000..60aad243c5 --- /dev/null +++ b/engine/remote.go @@ -0,0 +1,120 @@ +package engine + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/beam" + "github.com/dotcloud/docker/pkg/beam/data" + "io" + "os" + "strconv" + "sync" +) + +type Sender struct { + beam.Sender +} + +func NewSender(s beam.Sender) *Sender { + return &Sender{s} +} + +func (s *Sender) Install(eng *Engine) error { + // FIXME: this doesn't exist yet. + eng.RegisterCatchall(s.Handle) + return nil +} + +func (s *Sender) Handle(job *Job) Status { + msg := data.Empty().Set("cmd", append([]string{job.Name}, job.Args...)...) + peer, err := beam.SendConn(s, msg.Bytes()) + if err != nil { + return job.Errorf("beamsend: %v", err) + } + defer peer.Close() + var tasks sync.WaitGroup + defer tasks.Wait() + r := beam.NewRouter(nil) + r.NewRoute().KeyStartsWith("cmd", "log", "stdout").HasAttachment().Handler(func(p []byte, stdout *os.File) error { + tasks.Add(1) + io.Copy(job.Stdout, stdout) + tasks.Done() + return nil + }) + r.NewRoute().KeyStartsWith("cmd", "log", "stderr").HasAttachment().Handler(func(p []byte, stderr *os.File) error { + tasks.Add(1) + io.Copy(job.Stderr, stderr) + tasks.Done() + return nil + }) + r.NewRoute().KeyStartsWith("cmd", "log", "stdin").HasAttachment().Handler(func(p []byte, stdin *os.File) error { + tasks.Add(1) + io.Copy(stdin, job.Stdin) + tasks.Done() + return nil + }) + var status int + r.NewRoute().KeyStartsWith("cmd", "status").Handler(func(p []byte, f *os.File) error { + cmd := data.Message(p).Get("cmd") + if len(cmd) != 2 { + return fmt.Errorf("usage: %s <0-127>", cmd[0]) + } + s, err := strconv.ParseUint(cmd[1], 10, 8) + if err != nil { + return fmt.Errorf("usage: %s <0-127>", cmd[0]) + } + status = int(s) + return nil + + }) + if _, err := beam.Copy(r, peer); err != nil { + return job.Errorf("%v", err) + } + return Status(status) +} + +type Receiver struct { + *Engine + peer beam.Receiver +} + +func NewReceiver(peer beam.Receiver) *Receiver { + return &Receiver{Engine: New(), peer: peer} +} + +func (rcv *Receiver) Run() error { + r := beam.NewRouter(nil) + r.NewRoute().KeyExists("cmd").Handler(func(p []byte, f *os.File) error { + // Use the attachment as a beam return channel + peer, err := beam.FileConn(f) + if err != nil { + f.Close() + return err + } + cmd := data.Message(p).Get("cmd") + job := rcv.Engine.Job(cmd[0], cmd[1:]...) + stdout, err := beam.SendPipe(peer, data.Empty().Set("cmd", "log", "stdout").Bytes()) + if err != nil { + return err + } + job.Stdout.Add(stdout) + stderr, err := beam.SendPipe(peer, data.Empty().Set("cmd", "log", "stderr").Bytes()) + if err != nil { + return err + } + job.Stderr.Add(stderr) + stdin, err := beam.SendPipe(peer, data.Empty().Set("cmd", "log", "stdin").Bytes()) + if err != nil { + return err + } + job.Stdin.Add(stdin) + // ignore error because we pass the raw status + job.Run() + err = peer.Send(data.Empty().Set("cmd", "status", fmt.Sprintf("%d", job.status)).Bytes(), nil) + if err != nil { + return err + } + return nil + }) + _, err := beam.Copy(r, rcv.peer) + return err +} diff --git a/engine/remote_test.go b/engine/remote_test.go new file mode 100644 index 0000000000..54092ec934 --- /dev/null +++ b/engine/remote_test.go @@ -0,0 +1,3 @@ +package engine + +import () diff --git a/engine/rengine/main.go b/engine/rengine/main.go new file mode 100644 index 0000000000..b4fa01d39c --- /dev/null +++ b/engine/rengine/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "github.com/dotcloud/docker/engine" + "github.com/dotcloud/docker/pkg/beam" + "net" + "os" +) + +func main() { + eng := engine.New() + + c, err := net.Dial("unix", "beam.sock") + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + defer c.Close() + f, err := c.(*net.UnixConn).File() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + + child, err := beam.FileConn(f) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + defer child.Close() + + sender := engine.NewSender(child) + sender.Install(eng) + + cmd := eng.Job(os.Args[1], os.Args[2:]...) + cmd.Stdout.Add(os.Stdout) + cmd.Stderr.Add(os.Stderr) + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/engine/spawn/spawn.go b/engine/spawn/spawn.go new file mode 100644 index 0000000000..6680845bc1 --- /dev/null +++ b/engine/spawn/spawn.go @@ -0,0 +1,119 @@ +package spawn + +import ( + "fmt" + "github.com/dotcloud/docker/engine" + "github.com/dotcloud/docker/pkg/beam" + "github.com/dotcloud/docker/utils" + "os" + "os/exec" +) + +var initCalled bool + +// Init checks if the current process has been created by Spawn. +// +// If no, it returns nil and the original program can continue +// unmodified. +// +// If no, it hijacks the process to run as a child worker controlled +// by its parent over a beam connection, with f exposed as a remote +// service. In this case Init never returns. +// +// The hijacking process takes place as follows: +// - Open file descriptor 3 as a beam endpoint. If this fails, +// terminate the current process. +// - Start a new engine. +// - Call f.Install on the engine. Any handlers registered +// will be available for remote invocation by the parent. +// - Listen for beam messages from the parent and pass them to +// the handlers. +// - When the beam endpoint is closed by the parent, terminate +// the current process. +// +// NOTE: Init must be called at the beginning of the same program +// calling Spawn. This is because Spawn approximates a "fork" by +// re-executing the current binary - where it expects spawn.Init +// to intercept the control flow and execute the worker code. +func Init(f engine.Installer) error { + initCalled = true + if os.Getenv("ENGINESPAWN") != "1" { + return nil + } + fmt.Printf("[%d child]\n", os.Getpid()) + // Hijack the process + childErr := func() error { + fd3 := os.NewFile(3, "beam-introspect") + introsp, err := beam.FileConn(fd3) + if err != nil { + return fmt.Errorf("beam introspection error: %v", err) + } + fd3.Close() + defer introsp.Close() + eng := engine.NewReceiver(introsp) + if err := f.Install(eng.Engine); err != nil { + return err + } + if err := eng.Run(); err != nil { + return err + } + return nil + }() + if childErr != nil { + os.Exit(1) + } + os.Exit(0) + return nil // Never reached +} + +// Spawn starts a new Engine in a child process and returns +// a proxy Engine through which it can be controlled. +// +// The commands available on the child engine are determined +// by an earlier call to Init. It is important that Init be +// called at the very beginning of the current program - this +// allows it to be called as a re-execution hook in the child +// process. +// +// Long story short, if you want to expose `myservice` in a child +// process, do this: +// +// func main() { +// spawn.Init(myservice) +// [..] +// child, err := spawn.Spawn() +// [..] +// child.Job("dosomething").Run() +// } +func Spawn() (*engine.Engine, error) { + if !initCalled { + return nil, fmt.Errorf("spawn.Init must be called at the top of the main() function") + } + cmd := exec.Command(utils.SelfPath()) + cmd.Env = append(cmd.Env, "ENGINESPAWN=1") + local, remote, err := beam.SocketPair() + if err != nil { + return nil, err + } + child, err := beam.FileConn(local) + if err != nil { + local.Close() + remote.Close() + return nil, err + } + local.Close() + cmd.ExtraFiles = append(cmd.ExtraFiles, remote) + // FIXME: the beam/engine glue has no way to inform the caller + // of the child's termination. The next call will simply return + // an error. + if err := cmd.Start(); err != nil { + child.Close() + return nil, err + } + eng := engine.New() + if err := engine.NewSender(child).Install(eng); err != nil { + child.Close() + return nil, err + } + return eng, nil +} diff --git a/engine/spawn/subengine/main.go b/engine/spawn/subengine/main.go new file mode 100644 index 0000000000..3be7520a67 --- /dev/null +++ b/engine/spawn/subengine/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "github.com/dotcloud/docker/engine" + "github.com/dotcloud/docker/engine/spawn" + "log" + "os" + "os/exec" + "strings" +) + +func main() { + fmt.Printf("[%d] MAIN\n", os.Getpid()) + spawn.Init(&Worker{}) + fmt.Printf("[%d parent] spawning\n", os.Getpid()) + eng, err := spawn.Spawn() + if err != nil { + log.Fatal(err) + } + fmt.Printf("[parent] spawned\n") + job := eng.Job(os.Args[1], os.Args[2:]...) + job.Stdout.Add(os.Stdout) + job.Stderr.Add(os.Stderr) + job.Run() + // FIXME: use the job's status code + os.Exit(0) +} + +type Worker struct { +} + +func (w *Worker) Install(eng *engine.Engine) error { + eng.Register("exec", w.Exec) + eng.Register("cd", w.Cd) + eng.Register("echo", w.Echo) + return nil +} + +func (w *Worker) Exec(job *engine.Job) engine.Status { + fmt.Printf("--> %v\n", job.Args) + cmd := exec.Command(job.Args[0], job.Args[1:]...) + cmd.Stdout = job.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return job.Errorf("%v\n", err) + } + return engine.StatusOK +} + +func (w *Worker) Cd(job *engine.Job) engine.Status { + if err := os.Chdir(job.Args[0]); err != nil { + return job.Errorf("%v\n", err) + } + return engine.StatusOK +} + +func (w *Worker) Echo(job *engine.Job) engine.Status { + fmt.Fprintf(job.Stdout, "%s\n", strings.Join(job.Args, " ")) + return engine.StatusOK +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index d356f5f4de..5973f2fe1b 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -665,3 +665,25 @@ func TestUnPrivilegedCannotMount(t *testing.T) { logDone("run - test un-privileged cannot mount") } + +func TestSysNotAvaliableInNonPrivilegedContainers(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "busybox", "ls", "/sys/kernel") + if code, err := runCommand(cmd); err == nil || code == 0 { + t.Fatal("sys should not be available in a non privileged container") + } + + deleteAllContainers() + + logDone("run - sys not avaliable in non privileged container") +} + +func TestSysAvaliableInPrivilegedContainers(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "--privileged", "busybox", "ls", "/sys/kernel") + if code, err := runCommand(cmd); err != nil || code != 0 { + t.Fatalf("sys should be available in privileged container") + } + + deleteAllContainers() + + logDone("run - sys avaliable in privileged container") +} diff --git a/integration/MAINTAINERS b/integration/MAINTAINERS new file mode 100644 index 0000000000..2d47d7a711 --- /dev/null +++ b/integration/MAINTAINERS @@ -0,0 +1,4 @@ +Solomon Hykes +# WE ARE LOOKING FOR VOLUNTEERS TO HELP CLEAN THIS UP. +# TO VOLUNTEER PLEASE OPEN A PULL REQUEST ADDING YOURSELF TO THIS FILE. +# WE WILL HELP YOU GET STARTED. THANKS! diff --git a/integration/README.md b/integration/README.md new file mode 100644 index 0000000000..41f43a4ba7 --- /dev/null +++ b/integration/README.md @@ -0,0 +1,23 @@ +## Legacy integration tests + +`./integration` contains Docker's legacy integration tests. +It is DEPRECATED and will eventually be removed. + +### If you are a *CONTRIBUTOR* and want to add a test: + +* Consider mocking out side effects and contributing a *unit test* in the subsystem +you're modifying. For example, the remote API has unit tests in `./api/server/server_unit_tests.go`. +The events subsystem has unit tests in `./events/events_test.go`. And so on. + +* For end-to-end integration tests, please contribute to `./integration-cli`. + + +### If you are a *MAINTAINER* + +Please don't allow patches adding new tests to `./integration`. + +### If you are *LOOKING FOR A WAY TO HELP* + +Please consider porting tests away from `./integration` and into either unit tests or CLI tests. + +Any help will be greatly appreciated! diff --git a/integration/container_test.go b/integration/container_test.go index 67b2783ce9..8fe52a3cd6 100644 --- a/integration/container_test.go +++ b/integration/container_test.go @@ -407,7 +407,7 @@ func TestCopyVolumeUidGid(t *testing.T) { defer r.Nuke() // Add directory not owned by root - container1, _, _ := mkContainer(r, []string{"_", "/bin/sh", "-c", "mkdir -p /hello && touch /hello/test.txt && chown daemon.daemon /hello"}, t) + container1, _, _ := mkContainer(r, []string{"_", "/bin/sh", "-c", "mkdir -p /hello && touch /hello/test && chown daemon.daemon /hello"}, t) defer r.Destroy(container1) if container1.State.IsRunning() { @@ -432,6 +432,32 @@ func TestCopyVolumeUidGid(t *testing.T) { if !strings.Contains(stdout1, "daemon daemon") { t.Fatal("Container failed to transfer uid and gid to volume") } + + container2, _, _ := mkContainer(r, []string{"_", "/bin/sh", "-c", "mkdir -p /hello && chown daemon.daemon /hello"}, t) + defer r.Destroy(container1) + + if container2.State.IsRunning() { + t.Errorf("Container shouldn't be running") + } + if err := container2.Run(); err != nil { + t.Fatal(err) + } + if container2.State.IsRunning() { + t.Errorf("Container shouldn't be running") + } + + img2, err := r.Commit(container2, "", "", "unit test commited image", "", nil) + if err != nil { + t.Error(err) + } + + // Test that the uid and gid is copied from the image to the volume + tmpDir2 := tempDir(t) + defer os.RemoveAll(tmpDir2) + stdout2, _ := runContainer(eng, r, []string{"-v", "/hello", img2.ID, "stat", "-c", "%U %G", "/hello"}, t) + if !strings.Contains(stdout2, "daemon daemon") { + t.Fatal("Container failed to transfer uid and gid to volume") + } } // Test for #1582 diff --git a/pkg/cgroups/fs/blkio_test.go b/pkg/cgroups/fs/blkio_test.go new file mode 100644 index 0000000000..5279ac437b --- /dev/null +++ b/pkg/cgroups/fs/blkio_test.go @@ -0,0 +1,169 @@ +package fs + +import ( + "testing" +) + +const ( + sectorsRecursiveContents = `8:0 1024` + serviceBytesRecursiveContents = `8:0 Read 100 +8:0 Write 400 +8:0 Sync 200 +8:0 Async 300 +8:0 Total 500 +Total 500` + servicedRecursiveContents = `8:0 Read 10 +8:0 Write 40 +8:0 Sync 20 +8:0 Async 30 +8:0 Total 50 +Total 50` + queuedRecursiveContents = `8:0 Read 1 +8:0 Write 4 +8:0 Sync 2 +8:0 Async 3 +8:0 Total 5 +Total 5` +) + +func TestBlkioStats(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": serviceBytesRecursiveContents, + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + stats, err := blkio.Stats(helper.CgroupData) + if err != nil { + t.Fatal(err) + } + + // Verify expected stats. + expectedStats := map[string]float64{ + "blkio.sectors_recursive:8:0": 1024.0, + + // Serviced bytes. + "io_service_bytes_recursive:8:0:Read": 100.0, + "io_service_bytes_recursive:8:0:Write": 400.0, + "io_service_bytes_recursive:8:0:Sync": 200.0, + "io_service_bytes_recursive:8:0:Async": 300.0, + "io_service_bytes_recursive:8:0:Total": 500.0, + + // Serviced requests. + "io_serviced_recursive:8:0:Read": 10.0, + "io_serviced_recursive:8:0:Write": 40.0, + "io_serviced_recursive:8:0:Sync": 20.0, + "io_serviced_recursive:8:0:Async": 30.0, + "io_serviced_recursive:8:0:Total": 50.0, + + // Queued requests. + "io_queued_recursive:8:0:Read": 1.0, + "io_queued_recursive:8:0:Write": 4.0, + "io_queued_recursive:8:0:Sync": 2.0, + "io_queued_recursive:8:0:Async": 3.0, + "io_queued_recursive:8:0:Total": 5.0, + } + expectStats(t, expectedStats, stats) +} + +func TestBlkioStatsNoSectorsFile(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": serviceBytesRecursiveContents, + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} + +func TestBlkioStatsNoServiceBytesFile(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} + +func TestBlkioStatsNoServicedFile(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": serviceBytesRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} + +func TestBlkioStatsNoQueuedFile(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": serviceBytesRecursiveContents, + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} + +func TestBlkioStatsUnexpectedNumberOfFields(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": "8:0 Read 100 100", + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} + +func TestBlkioStatsUnexpectedFieldType(t *testing.T) { + helper := NewCgroupTestUtil("blkio", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "blkio.io_service_bytes_recursive": "8:0 Read Write", + "blkio.io_serviced_recursive": servicedRecursiveContents, + "blkio.io_queued_recursive": queuedRecursiveContents, + "blkio.sectors_recursive": sectorsRecursiveContents, + }) + + blkio := &blkioGroup{} + _, err := blkio.Stats(helper.CgroupData) + if err == nil { + t.Fatal("Expected to fail, but did not") + } +} diff --git a/pkg/cgroups/fs/memory.go b/pkg/cgroups/fs/memory.go index 5315291197..837640c088 100644 --- a/pkg/cgroups/fs/memory.go +++ b/pkg/cgroups/fs/memory.go @@ -2,6 +2,7 @@ package fs import ( "bufio" + "fmt" "os" "path/filepath" "strconv" @@ -56,13 +57,14 @@ func (s *memoryGroup) Stats(d *data) (map[string]float64, error) { return nil, err } - f, err := os.Open(filepath.Join(path, "memory.stat")) + // Set stats from memory.stat. + statsFile, err := os.Open(filepath.Join(path, "memory.stat")) if err != nil { return nil, err } - defer f.Close() + defer statsFile.Close() - sc := bufio.NewScanner(f) + sc := bufio.NewScanner(statsFile) for sc.Scan() { t, v, err := getCgroupParamKeyValue(sc.Text()) if err != nil { @@ -70,5 +72,19 @@ func (s *memoryGroup) Stats(d *data) (map[string]float64, error) { } paramData[t] = v } + + // Set memory usage and max historical usage. + params := []string{ + "usage_in_bytes", + "max_usage_in_bytes", + } + for _, param := range params { + value, err := getCgroupParamFloat64(path, fmt.Sprintf("memory.%s", param)) + if err != nil { + return nil, err + } + paramData[param] = value + } + return paramData, nil } diff --git a/pkg/cgroups/fs/test_util.go b/pkg/cgroups/fs/test_util.go new file mode 100644 index 0000000000..11b90b21d6 --- /dev/null +++ b/pkg/cgroups/fs/test_util.go @@ -0,0 +1,75 @@ +/* +Utility for testing cgroup operations. + +Creates a mock of the cgroup filesystem for the duration of the test. +*/ +package fs + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "testing" +) + +type cgroupTestUtil struct { + // data to use in tests. + CgroupData *data + + // Path to the mock cgroup directory. + CgroupPath string + + // Temporary directory to store mock cgroup filesystem. + tempDir string + t *testing.T +} + +// Creates a new test util for the specified subsystem +func NewCgroupTestUtil(subsystem string, t *testing.T) *cgroupTestUtil { + d := &data{} + tempDir, err := ioutil.TempDir("", fmt.Sprintf("%s_cgroup_test", subsystem)) + if err != nil { + t.Fatal(err) + } + d.root = tempDir + testCgroupPath, err := d.path(subsystem) + if err != nil { + t.Fatal(err) + } + + // Ensure the full mock cgroup path exists. + err = os.MkdirAll(testCgroupPath, 0755) + if err != nil { + t.Fatal(err) + } + return &cgroupTestUtil{CgroupData: d, CgroupPath: testCgroupPath, tempDir: tempDir, t: t} +} + +func (c *cgroupTestUtil) cleanup() { + os.RemoveAll(c.tempDir) +} + +// Write the specified contents on the mock of the specified cgroup files. +func (c *cgroupTestUtil) writeFileContents(fileContents map[string]string) { + for file, contents := range fileContents { + err := writeFile(c.CgroupPath, file, contents) + if err != nil { + c.t.Fatal(err) + } + } +} + +// Expect the specified stats. +func expectStats(t *testing.T, expected, actual map[string]float64) { + for stat, expectedValue := range expected { + actualValue, ok := actual[stat] + if !ok { + log.Printf("Expected stat %s to exist: %s", stat, actual) + t.Fail() + } else if actualValue != expectedValue { + log.Printf("Expected stats %s to have value %f but had %f instead", stat, expectedValue, actualValue) + t.Fail() + } + } +} diff --git a/pkg/cgroups/fs/utils.go b/pkg/cgroups/fs/utils.go index f4c4846b8c..8be65c97ea 100644 --- a/pkg/cgroups/fs/utils.go +++ b/pkg/cgroups/fs/utils.go @@ -3,6 +3,8 @@ package fs import ( "errors" "fmt" + "io/ioutil" + "path/filepath" "strconv" "strings" ) @@ -27,3 +29,12 @@ func getCgroupParamKeyValue(t string) (string, float64, error) { return "", 0.0, ErrNotValidFormat } } + +// Gets a single float64 value from the specified cgroup file. +func getCgroupParamFloat64(cgroupPath, cgroupFile string) (float64, error) { + contents, err := ioutil.ReadFile(filepath.Join(cgroupPath, cgroupFile)) + if err != nil { + return -1.0, err + } + return strconv.ParseFloat(strings.TrimSpace(string(contents)), 64) +} diff --git a/pkg/cgroups/fs/utils_test.go b/pkg/cgroups/fs/utils_test.go new file mode 100644 index 0000000000..c8f1b0172b --- /dev/null +++ b/pkg/cgroups/fs/utils_test.go @@ -0,0 +1,68 @@ +package fs + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +const ( + cgroupFile = "cgroup.file" + floatValue = 2048.0 + floatString = "2048" +) + +func TestGetCgroupParamsFloat64(t *testing.T) { + // Setup tempdir. + tempDir, err := ioutil.TempDir("", "cgroup_utils_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempFile := filepath.Join(tempDir, cgroupFile) + + // Success. + err = ioutil.WriteFile(tempFile, []byte(floatString), 0755) + if err != nil { + t.Fatal(err) + } + value, err := getCgroupParamFloat64(tempDir, cgroupFile) + if err != nil { + t.Fatal(err) + } else if value != floatValue { + t.Fatalf("Expected %f to equal %f", value, floatValue) + } + + // Success with new line. + err = ioutil.WriteFile(tempFile, []byte(floatString+"\n"), 0755) + if err != nil { + t.Fatal(err) + } + value, err = getCgroupParamFloat64(tempDir, cgroupFile) + if err != nil { + t.Fatal(err) + } else if value != floatValue { + t.Fatalf("Expected %f to equal %f", value, floatValue) + } + + // Not a float. + err = ioutil.WriteFile(tempFile, []byte("not-a-float"), 0755) + if err != nil { + t.Fatal(err) + } + _, err = getCgroupParamFloat64(tempDir, cgroupFile) + if err == nil { + t.Fatal("Expecting error, got none") + } + + // Unknown file. + err = os.Remove(tempFile) + if err != nil { + t.Fatal(err) + } + _, err = getCgroupParamFloat64(tempDir, cgroupFile) + if err == nil { + t.Fatal("Expecting error, got none") + } +} diff --git a/pkg/libcontainer/README.md b/pkg/libcontainer/README.md index 70f22f5639..31031b26cd 100644 --- a/pkg/libcontainer/README.md +++ b/pkg/libcontainer/README.md @@ -16,135 +16,148 @@ process are specified in this file. The configuration is used for each process Sample `container.json` file: ```json { + "mounts" : [ + { + "type" : "devtmpfs" + } + ], + "tty" : true, + "environment" : [ + "HOME=/", + "PATH=PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin", + "container=docker", + "TERM=xterm-256color" + ], "hostname" : "koye", + "cgroups" : { + "parent" : "docker", + "name" : "docker-koye" + }, + "capabilities_mask" : [ + { + "value" : 8, + "key" : "SETPCAP", + "enabled" : false + }, + { + "enabled" : false, + "value" : 16, + "key" : "SYS_MODULE" + }, + { + "value" : 17, + "key" : "SYS_RAWIO", + "enabled" : false + }, + { + "key" : "SYS_PACCT", + "value" : 20, + "enabled" : false + }, + { + "value" : 21, + "key" : "SYS_ADMIN", + "enabled" : false + }, + { + "value" : 23, + "key" : "SYS_NICE", + "enabled" : false + }, + { + "value" : 24, + "key" : "SYS_RESOURCE", + "enabled" : false + }, + { + "key" : "SYS_TIME", + "value" : 25, + "enabled" : false + }, + { + "enabled" : false, + "value" : 26, + "key" : "SYS_TTY_CONFIG" + }, + { + "key" : "AUDIT_WRITE", + "value" : 29, + "enabled" : false + }, + { + "value" : 30, + "key" : "AUDIT_CONTROL", + "enabled" : false + }, + { + "enabled" : false, + "key" : "MAC_OVERRIDE", + "value" : 32 + }, + { + "enabled" : false, + "key" : "MAC_ADMIN", + "value" : 33 + }, + { + "key" : "NET_ADMIN", + "value" : 12, + "enabled" : false + }, + { + "value" : 27, + "key" : "MKNOD", + "enabled" : true + } + ], "networks" : [ { - "gateway" : "172.17.42.1", + "mtu" : 1500, + "address" : "127.0.0.1/0", + "type" : "loopback", + "gateway" : "localhost" + }, + { + "mtu" : 1500, + "address" : "172.17.42.2/16", + "type" : "veth", "context" : { "bridge" : "docker0", "prefix" : "veth" }, - "address" : "172.17.0.2/16", - "type" : "veth", - "mtu" : 1500 - } - ], - "cgroups" : { - "parent" : "docker", - "name" : "11bb30683fb0bdd57fab4d3a8238877f1e4395a2cfc7320ea359f7a02c1a5620" - }, - "tty" : true, - "environment" : [ - "HOME=/", - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "HOSTNAME=11bb30683fb0", - "TERM=xterm" - ], - "capabilities_mask" : [ - { - "key": "SETPCAP", - "enabled": false - }, - { - "key": "SYS_MODULE", - "enabled": false - }, - { - "key": "SYS_RAWIO", - "enabled": false - }, - { - "key": "SYS_PACCT", - "enabled": false - }, - { - "key": "SYS_ADMIN", - "enabled": false - }, - { - "key": "SYS_NICE", - "enabled": false - }, - { - "key": "SYS_RESOURCE", - "enabled": false - }, - { - "key": "SYS_TIME", - "enabled": false - }, - { - "key": "SYS_TTY_CONFIG", - "enabled": false - }, - { - "key": "MKNOD", - "enabled": true - }, - { - "key": "AUDIT_WRITE", - "enabled": false - }, - { - "key": "AUDIT_CONTROL", - "enabled": false - }, - { - "key": "MAC_OVERRIDE", - "enabled": false - }, - { - "key": "MAC_ADMIN", - "enabled": false - }, - { - "key": "NET_ADMIN", - "enabled": false - } - ], - "context" : { - "apparmor_profile" : "docker-default" - }, - "mounts" : [ - { - "source" : "/var/lib/docker/containers/11bb30683fb0bdd57fab4d3a8238877f1e4395a2cfc7320ea359f7a02c1a5620/resolv.conf", - "writable" : false, - "destination" : "/etc/resolv.conf", - "private" : true - }, - { - "source" : "/var/lib/docker/containers/11bb30683fb0bdd57fab4d3a8238877f1e4395a2cfc7320ea359f7a02c1a5620/hostname", - "writable" : false, - "destination" : "/etc/hostname", - "private" : true - }, - { - "source" : "/var/lib/docker/containers/11bb30683fb0bdd57fab4d3a8238877f1e4395a2cfc7320ea359f7a02c1a5620/hosts", - "writable" : false, - "destination" : "/etc/hosts", - "private" : true + "gateway" : "172.17.42.1" } ], "namespaces" : [ { - "key": "NEWNS", - "enabled": true + "key" : "NEWNS", + "value" : 131072, + "enabled" : true, + "file" : "mnt" }, { - "key": "NEWUTS", - "enabled": true + "key" : "NEWUTS", + "value" : 67108864, + "enabled" : true, + "file" : "uts" }, { - "key": "NEWIPC", - "enabled": true + "enabled" : true, + "file" : "ipc", + "key" : "NEWIPC", + "value" : 134217728 }, { - "key": "NEWPID", - "enabled": true + "file" : "pid", + "enabled" : true, + "value" : 536870912, + "key" : "NEWPID" }, { - "key": "NEWNET", - "enabled": true + "enabled" : true, + "file" : "net", + "key" : "NEWNET", + "value" : 1073741824 } ] } diff --git a/pkg/libcontainer/console/console.go b/pkg/libcontainer/console/console.go new file mode 100644 index 0000000000..05cd08a92e --- /dev/null +++ b/pkg/libcontainer/console/console.go @@ -0,0 +1,60 @@ +// +build linux + +package console + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/label" + "github.com/dotcloud/docker/pkg/system" + "os" + "path/filepath" + "syscall" +) + +// Setup initializes the proper /dev/console inside the rootfs path +func Setup(rootfs, consolePath, mountLabel string) error { + oldMask := system.Umask(0000) + defer system.Umask(oldMask) + + stat, err := os.Stat(consolePath) + if err != nil { + return fmt.Errorf("stat console %s %s", consolePath, err) + } + var ( + st = stat.Sys().(*syscall.Stat_t) + dest = filepath.Join(rootfs, "dev/console") + ) + if err := os.Remove(dest); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove %s %s", dest, err) + } + if err := os.Chmod(consolePath, 0600); err != nil { + return err + } + if err := os.Chown(consolePath, 0, 0); err != nil { + return err + } + if err := system.Mknod(dest, (st.Mode&^07777)|0600, int(st.Rdev)); err != nil { + return fmt.Errorf("mknod %s %s", dest, err) + } + if err := label.SetFileLabel(consolePath, mountLabel); err != nil { + return fmt.Errorf("set file label %s %s", dest, err) + } + if err := system.Mount(consolePath, dest, "bind", syscall.MS_BIND, ""); err != nil { + return fmt.Errorf("bind %s to %s %s", consolePath, dest, err) + } + return nil +} + +func OpenAndDup(consolePath string) error { + slave, err := system.OpenTerminal(consolePath, syscall.O_RDWR) + if err != nil { + return fmt.Errorf("open terminal %s", err) + } + if err := system.Dup2(slave.Fd(), 0); err != nil { + return err + } + if err := system.Dup2(slave.Fd(), 1); err != nil { + return err + } + return system.Dup2(slave.Fd(), 2) +} diff --git a/pkg/libcontainer/container.go b/pkg/libcontainer/container.go index c7cac35428..ddcc6cab70 100644 --- a/pkg/libcontainer/container.go +++ b/pkg/libcontainer/container.go @@ -23,7 +23,7 @@ type Container struct { Networks []*Network `json:"networks,omitempty"` // nil for host's network stack Cgroups *cgroups.Cgroup `json:"cgroups,omitempty"` // cgroups Context Context `json:"context,omitempty"` // generic context for specific options (apparmor, selinux) - Mounts []Mount `json:"mounts,omitempty"` + Mounts Mounts `json:"mounts,omitempty"` } // Network defines configuration for a container's networking stack @@ -37,12 +37,3 @@ type Network struct { Gateway string `json:"gateway,omitempty"` Mtu int `json:"mtu,omitempty"` } - -// Bind mounts from the host system to the container -// -type Mount struct { - Source string `json:"source"` // Source path, in the host namespace - Destination string `json:"destination"` // Destination path, in the container - Writable bool `json:"writable"` - Private bool `json:"private"` -} diff --git a/pkg/libcontainer/container.json b/pkg/libcontainer/container.json index 7c69a180fe..f15a49ab05 100644 --- a/pkg/libcontainer/container.json +++ b/pkg/libcontainer/container.json @@ -1,129 +1,146 @@ { - "hostname": "koye", - "tty": true, - "environment": [ - "HOME=/", - "PATH=PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin", - "container=docker", - "TERM=xterm-256color" - ], - "namespaces": [ - { - "file": "ipc", - "value": 134217728, - "enabled": true, - "key": "NEWIPC" - }, - { - "file": "mnt", - "value": 131072, - "enabled": true, - "key": "NEWNS" - }, - { - "file": "pid", - "value": 536870912, - "enabled": true, - "key": "NEWPID" - }, - { - "file": "uts", - "value": 67108864, - "enabled": true, - "key": "NEWUTS" - }, - { - "file": "net", - "value": 1073741824, - "enabled": true, - "key": "NEWNET" - } - ], - "capabilities_mask": [ - { - "key": "SETPCAP", - "enabled": false - }, - { - "key": "SYS_MODULE", - "enabled": false - }, - { - "key": "SYS_RAWIO", - "enabled": false - }, - { - "key": "SYS_PACCT", - "enabled": false - }, - { - "key": "SYS_ADMIN", - "enabled": false - }, - { - "key": "SYS_NICE", - "enabled": false - }, - { - "key": "SYS_RESOURCE", - "enabled": false - }, - { - "key": "SYS_TIME", - "enabled": false - }, - { - "key": "SYS_TTY_CONFIG", - "enabled": false - }, - { - "key": "MKNOD", - "enabled": true - }, - { - "key": "AUDIT_WRITE", - "enabled": false - }, - { - "key": "AUDIT_CONTROL", - "enabled": false - }, - { - "key": "MAC_OVERRIDE", - "enabled": false - }, - { - "key": "MAC_ADMIN", - "enabled": false - }, - { - "key": "NET_ADMIN", - "enabled": false - } - ], - "networks": [ - { - "type": "loopback", - "gateway": "localhost", - "address": "127.0.0.1/0", - "mtu": 1500 - }, - { - "type": "veth", - "gateway": "172.17.42.1", - "address": "172.17.0.4/16", - "context": { - "prefix": "dock", - "bridge": "docker0" - }, - "mtu": 1500 - } - ], - "cgroups": { - "name": "docker-koye", - "parent": "docker", - "memory": 5248000, - "memory_swap": -1, - "cpu_shares": 1024 - } + "mounts" : [ + { + "type" : "devtmpfs" + } + ], + "tty" : true, + "environment" : [ + "HOME=/", + "PATH=PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin", + "container=docker", + "TERM=xterm-256color" + ], + "hostname" : "koye", + "cgroups" : { + "parent" : "docker", + "name" : "docker-koye" + }, + "capabilities_mask" : [ + { + "value" : 8, + "key" : "SETPCAP", + "enabled" : false + }, + { + "enabled" : false, + "value" : 16, + "key" : "SYS_MODULE" + }, + { + "value" : 17, + "key" : "SYS_RAWIO", + "enabled" : false + }, + { + "key" : "SYS_PACCT", + "value" : 20, + "enabled" : false + }, + { + "value" : 21, + "key" : "SYS_ADMIN", + "enabled" : false + }, + { + "value" : 23, + "key" : "SYS_NICE", + "enabled" : false + }, + { + "value" : 24, + "key" : "SYS_RESOURCE", + "enabled" : false + }, + { + "key" : "SYS_TIME", + "value" : 25, + "enabled" : false + }, + { + "enabled" : false, + "value" : 26, + "key" : "SYS_TTY_CONFIG" + }, + { + "key" : "AUDIT_WRITE", + "value" : 29, + "enabled" : false + }, + { + "value" : 30, + "key" : "AUDIT_CONTROL", + "enabled" : false + }, + { + "enabled" : false, + "key" : "MAC_OVERRIDE", + "value" : 32 + }, + { + "enabled" : false, + "key" : "MAC_ADMIN", + "value" : 33 + }, + { + "key" : "NET_ADMIN", + "value" : 12, + "enabled" : false + }, + { + "value" : 27, + "key" : "MKNOD", + "enabled" : true + } + ], + "networks" : [ + { + "mtu" : 1500, + "address" : "127.0.0.1/0", + "type" : "loopback", + "gateway" : "localhost" + }, + { + "mtu" : 1500, + "address" : "172.17.42.2/16", + "type" : "veth", + "context" : { + "bridge" : "docker0", + "prefix" : "veth" + }, + "gateway" : "172.17.42.1" + } + ], + "namespaces" : [ + { + "key" : "NEWNS", + "value" : 131072, + "enabled" : true, + "file" : "mnt" + }, + { + "key" : "NEWUTS", + "value" : 67108864, + "enabled" : true, + "file" : "uts" + }, + { + "enabled" : true, + "file" : "ipc", + "key" : "NEWIPC", + "value" : 134217728 + }, + { + "file" : "pid", + "enabled" : true, + "value" : 536870912, + "key" : "NEWPID" + }, + { + "enabled" : true, + "file" : "net", + "key" : "NEWNET", + "value" : 1073741824 + } + ] } diff --git a/pkg/libcontainer/container_test.go b/pkg/libcontainer/container_test.go index c413c7c34a..d710a6a53c 100644 --- a/pkg/libcontainer/container_test.go +++ b/pkg/libcontainer/container_test.go @@ -56,14 +56,4 @@ func TestContainerJsonFormat(t *testing.T) { t.Log("capabilities mask should not contain SYS_CHROOT") t.Fail() } - - if container.Cgroups.CpuShares != 1024 { - t.Log("cpu shares not set correctly") - t.Fail() - } - - if container.Cgroups.Memory != 5248000 { - t.Log("memory limit not set correctly") - t.Fail() - } } diff --git a/pkg/libcontainer/mount/init.go b/pkg/libcontainer/mount/init.go new file mode 100644 index 0000000000..06b2c82f56 --- /dev/null +++ b/pkg/libcontainer/mount/init.go @@ -0,0 +1,143 @@ +// +build linux + +package mount + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/label" + "github.com/dotcloud/docker/pkg/libcontainer" + "github.com/dotcloud/docker/pkg/libcontainer/mount/nodes" + "github.com/dotcloud/docker/pkg/libcontainer/security/restrict" + "github.com/dotcloud/docker/pkg/system" + "os" + "path/filepath" + "syscall" +) + +// default mount point flags +const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV + +type mount struct { + source string + path string + device string + flags int + data string +} + +// InitializeMountNamespace setups up the devices, mount points, and filesystems for use inside a +// new mount namepsace +func InitializeMountNamespace(rootfs, console string, container *libcontainer.Container) error { + var ( + err error + flag = syscall.MS_PRIVATE + ) + if container.NoPivotRoot { + flag = syscall.MS_SLAVE + } + if err := system.Mount("", "/", "", uintptr(flag|syscall.MS_REC), ""); err != nil { + return fmt.Errorf("mounting / as slave %s", err) + } + if err := system.Mount(rootfs, rootfs, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { + return fmt.Errorf("mouting %s as bind %s", rootfs, err) + } + if err := mountSystem(rootfs, container); err != nil { + return fmt.Errorf("mount system %s", err) + } + if err := setupBindmounts(rootfs, container.Mounts); err != nil { + return fmt.Errorf("bind mounts %s", err) + } + if err := nodes.CopyN(rootfs, nodes.DefaultNodes); err != nil { + return fmt.Errorf("copy dev nodes %s", err) + } + if restrictionPath := container.Context["restriction_path"]; restrictionPath != "" { + if err := restrict.Restrict(rootfs, restrictionPath); err != nil { + return fmt.Errorf("restrict %s", err) + } + } + if err := SetupPtmx(rootfs, console, container.Context["mount_label"]); err != nil { + return err + } + if err := system.Chdir(rootfs); err != nil { + return fmt.Errorf("chdir into %s %s", rootfs, err) + } + + if container.NoPivotRoot { + err = MsMoveRoot(rootfs) + } else { + err = PivotRoot(rootfs) + } + if err != nil { + return err + } + + if container.ReadonlyFs { + if err := SetReadonly(); err != nil { + return fmt.Errorf("set readonly %s", err) + } + } + + system.Umask(0022) + + return nil +} + +// mountSystem sets up linux specific system mounts like sys, proc, shm, and devpts +// inside the mount namespace +func mountSystem(rootfs string, container *libcontainer.Container) error { + for _, m := range newSystemMounts(rootfs, container.Context["mount_label"], container.Mounts) { + if err := os.MkdirAll(m.path, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("mkdirall %s %s", m.path, err) + } + if err := system.Mount(m.source, m.path, m.device, uintptr(m.flags), m.data); err != nil { + return fmt.Errorf("mounting %s into %s %s", m.source, m.path, err) + } + } + return nil +} + +func setupBindmounts(rootfs string, bindMounts libcontainer.Mounts) error { + for _, m := range bindMounts.OfType("bind") { + var ( + flags = syscall.MS_BIND | syscall.MS_REC + dest = filepath.Join(rootfs, m.Destination) + ) + if !m.Writable { + flags = flags | syscall.MS_RDONLY + } + if err := system.Mount(m.Source, dest, "bind", uintptr(flags), ""); err != nil { + return fmt.Errorf("mounting %s into %s %s", m.Source, dest, err) + } + if !m.Writable { + if err := system.Mount(m.Source, dest, "bind", uintptr(flags|syscall.MS_REMOUNT), ""); err != nil { + return fmt.Errorf("remounting %s into %s %s", m.Source, dest, err) + } + } + if m.Private { + if err := system.Mount("", dest, "none", uintptr(syscall.MS_PRIVATE), ""); err != nil { + return fmt.Errorf("mounting %s private %s", dest, err) + } + } + } + return nil +} + +// TODO: this is crappy right now and should be cleaned up with a better way of handling system and +// standard bind mounts allowing them to be more dymanic +func newSystemMounts(rootfs, mountLabel string, mounts libcontainer.Mounts) []mount { + systemMounts := []mount{ + {source: "proc", path: filepath.Join(rootfs, "proc"), device: "proc", flags: defaultMountFlags}, + } + + if len(mounts.OfType("devtmpfs")) == 1 { + systemMounts = append(systemMounts, mount{source: "tmpfs", path: filepath.Join(rootfs, "dev"), device: "tmpfs", flags: syscall.MS_NOSUID | syscall.MS_STRICTATIME, data: "mode=755"}) + } + systemMounts = append(systemMounts, + mount{source: "shm", path: filepath.Join(rootfs, "dev", "shm"), device: "tmpfs", flags: defaultMountFlags, data: label.FormatMountLabel("mode=1777,size=65536k", mountLabel)}, + mount{source: "devpts", path: filepath.Join(rootfs, "dev", "pts"), device: "devpts", flags: syscall.MS_NOSUID | syscall.MS_NOEXEC, data: label.FormatMountLabel("newinstance,ptmxmode=0666,mode=620,gid=5", mountLabel)}) + + if len(mounts.OfType("sysfs")) == 1 { + systemMounts = append(systemMounts, mount{source: "sysfs", path: filepath.Join(rootfs, "sys"), device: "sysfs", flags: defaultMountFlags}) + } + return systemMounts +} diff --git a/pkg/libcontainer/mount/msmoveroot.go b/pkg/libcontainer/mount/msmoveroot.go new file mode 100644 index 0000000000..b336c86495 --- /dev/null +++ b/pkg/libcontainer/mount/msmoveroot.go @@ -0,0 +1,19 @@ +// +build linux + +package mount + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/system" + "syscall" +) + +func MsMoveRoot(rootfs string) error { + if err := system.Mount(rootfs, "/", "", syscall.MS_MOVE, ""); err != nil { + return fmt.Errorf("mount move %s into / %s", rootfs, err) + } + if err := system.Chroot("."); err != nil { + return fmt.Errorf("chroot . %s", err) + } + return system.Chdir("/") +} diff --git a/pkg/libcontainer/mount/nodes/nodes.go b/pkg/libcontainer/mount/nodes/nodes.go new file mode 100644 index 0000000000..5022f85b0b --- /dev/null +++ b/pkg/libcontainer/mount/nodes/nodes.go @@ -0,0 +1,49 @@ +// +build linux + +package nodes + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/system" + "os" + "path/filepath" + "syscall" +) + +// Default list of device nodes to copy +var DefaultNodes = []string{ + "null", + "zero", + "full", + "random", + "urandom", + "tty", +} + +// CopyN copies the device node from the host into the rootfs +func CopyN(rootfs string, nodesToCopy []string) error { + oldMask := system.Umask(0000) + defer system.Umask(oldMask) + + for _, node := range nodesToCopy { + if err := Copy(rootfs, node); err != nil { + return err + } + } + return nil +} + +func Copy(rootfs, node string) error { + stat, err := os.Stat(filepath.Join("/dev", node)) + if err != nil { + return err + } + var ( + dest = filepath.Join(rootfs, "dev", node) + st = stat.Sys().(*syscall.Stat_t) + ) + if err := system.Mknod(dest, st.Mode, int(st.Rdev)); err != nil && !os.IsExist(err) { + return fmt.Errorf("copy %s %s", node, err) + } + return nil +} diff --git a/pkg/libcontainer/mount/pivotroot.go b/pkg/libcontainer/mount/pivotroot.go new file mode 100644 index 0000000000..447f5904b2 --- /dev/null +++ b/pkg/libcontainer/mount/pivotroot.go @@ -0,0 +1,31 @@ +// +build linux + +package mount + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/system" + "io/ioutil" + "os" + "path/filepath" + "syscall" +) + +func PivotRoot(rootfs string) error { + pivotDir, err := ioutil.TempDir(rootfs, ".pivot_root") + if err != nil { + return fmt.Errorf("can't create pivot_root dir %s", pivotDir, err) + } + if err := system.Pivotroot(rootfs, pivotDir); err != nil { + return fmt.Errorf("pivot_root %s", err) + } + if err := system.Chdir("/"); err != nil { + return fmt.Errorf("chdir / %s", err) + } + // path to pivot dir now changed, update + pivotDir = filepath.Join("/", filepath.Base(pivotDir)) + if err := system.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { + return fmt.Errorf("unmount pivot_root dir %s", err) + } + return os.Remove(pivotDir) +} diff --git a/pkg/libcontainer/mount/ptmx.go b/pkg/libcontainer/mount/ptmx.go new file mode 100644 index 0000000000..f6ca534637 --- /dev/null +++ b/pkg/libcontainer/mount/ptmx.go @@ -0,0 +1,26 @@ +// +build linux + +package mount + +import ( + "fmt" + "github.com/dotcloud/docker/pkg/libcontainer/console" + "os" + "path/filepath" +) + +func SetupPtmx(rootfs, consolePath, mountLabel string) error { + ptmx := filepath.Join(rootfs, "dev/ptmx") + if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) { + return err + } + if err := os.Symlink("pts/ptmx", ptmx); err != nil { + return fmt.Errorf("symlink dev ptmx %s", err) + } + if consolePath != "" { + if err := console.Setup(rootfs, consolePath, mountLabel); err != nil { + return err + } + } + return nil +} diff --git a/pkg/libcontainer/mount/readonly.go b/pkg/libcontainer/mount/readonly.go new file mode 100644 index 0000000000..0658358ad6 --- /dev/null +++ b/pkg/libcontainer/mount/readonly.go @@ -0,0 +1,12 @@ +// +build linux + +package mount + +import ( + "github.com/dotcloud/docker/pkg/system" + "syscall" +) + +func SetReadonly() error { + return system.Mount("/", "/", "bind", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_REC, "") +} diff --git a/pkg/libcontainer/mount/remount.go b/pkg/libcontainer/mount/remount.go new file mode 100644 index 0000000000..3e00509ae0 --- /dev/null +++ b/pkg/libcontainer/mount/remount.go @@ -0,0 +1,31 @@ +// +build linux + +package mount + +import ( + "github.com/dotcloud/docker/pkg/system" + "syscall" +) + +func RemountProc() error { + if err := system.Unmount("/proc", syscall.MNT_DETACH); err != nil { + return err + } + if err := system.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil { + return err + } + return nil +} + +func RemountSys() error { + if err := system.Unmount("/sys", syscall.MNT_DETACH); err != nil { + if err != syscall.EINVAL { + return err + } + } else { + if err := system.Mount("sysfs", "/sys", "sysfs", uintptr(defaultMountFlags), ""); err != nil { + return err + } + } + return nil +} diff --git a/pkg/libcontainer/nsinit/execin.go b/pkg/libcontainer/nsinit/execin.go index 9017af06e9..b79881015f 100644 --- a/pkg/libcontainer/nsinit/execin.go +++ b/pkg/libcontainer/nsinit/execin.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/dotcloud/docker/pkg/label" "github.com/dotcloud/docker/pkg/libcontainer" + "github.com/dotcloud/docker/pkg/libcontainer/mount" "github.com/dotcloud/docker/pkg/system" "os" "path/filepath" @@ -63,10 +64,10 @@ func (ns *linuxNs) ExecIn(container *libcontainer.Container, nspid int, args []s if err := system.Unshare(syscall.CLONE_NEWNS); err != nil { return -1, err } - if err := remountProc(); err != nil { + if err := mount.RemountProc(); err != nil { return -1, fmt.Errorf("remount proc %s", err) } - if err := remountSys(); err != nil { + if err := mount.RemountSys(); err != nil { return -1, fmt.Errorf("remount sys %s", err) } goto dropAndExec diff --git a/pkg/libcontainer/nsinit/init.go b/pkg/libcontainer/nsinit/init.go index 0e85c0e4be..67095fdba1 100644 --- a/pkg/libcontainer/nsinit/init.go +++ b/pkg/libcontainer/nsinit/init.go @@ -11,8 +11,10 @@ import ( "github.com/dotcloud/docker/pkg/apparmor" "github.com/dotcloud/docker/pkg/label" "github.com/dotcloud/docker/pkg/libcontainer" - "github.com/dotcloud/docker/pkg/libcontainer/capabilities" + "github.com/dotcloud/docker/pkg/libcontainer/console" + "github.com/dotcloud/docker/pkg/libcontainer/mount" "github.com/dotcloud/docker/pkg/libcontainer/network" + "github.com/dotcloud/docker/pkg/libcontainer/security/capabilities" "github.com/dotcloud/docker/pkg/libcontainer/utils" "github.com/dotcloud/docker/pkg/system" "github.com/dotcloud/docker/pkg/user" @@ -20,7 +22,7 @@ import ( // Init is the init process that first runs inside a new namespace to setup mounts, users, networking, // and other options required for the new container. -func (ns *linuxNs) Init(container *libcontainer.Container, uncleanRootfs, console string, syncPipe *SyncPipe, args []string) error { +func (ns *linuxNs) Init(container *libcontainer.Container, uncleanRootfs, consolePath string, syncPipe *SyncPipe, args []string) error { rootfs, err := utils.ResolveRootfs(uncleanRootfs) if err != nil { return err @@ -36,20 +38,16 @@ func (ns *linuxNs) Init(container *libcontainer.Container, uncleanRootfs, consol ns.logger.Println("received context from parent") syncPipe.Close() - if console != "" { - ns.logger.Printf("setting up %s as console\n", console) - slave, err := system.OpenTerminal(console, syscall.O_RDWR) - if err != nil { - return fmt.Errorf("open terminal %s", err) - } - if err := dupSlave(slave); err != nil { - return fmt.Errorf("dup2 slave %s", err) + if consolePath != "" { + ns.logger.Printf("setting up %s as console\n", consolePath) + if err := console.OpenAndDup(consolePath); err != nil { + return err } } if _, err := system.Setsid(); err != nil { return fmt.Errorf("setsid %s", err) } - if console != "" { + if consolePath != "" { if err := system.Setctty(); err != nil { return fmt.Errorf("setctty %s", err) } @@ -60,7 +58,7 @@ func (ns *linuxNs) Init(container *libcontainer.Container, uncleanRootfs, consol label.Init() ns.logger.Println("setup mount namespace") - if err := setupNewMountNamespace(rootfs, container.Mounts, console, container.ReadonlyFs, container.NoPivotRoot, container.Context["mount_label"]); err != nil { + if err := mount.InitializeMountNamespace(rootfs, consolePath, container); err != nil { return fmt.Errorf("setup mount namespace %s", err) } if err := system.Sethostname(container.Hostname); err != nil { @@ -114,21 +112,6 @@ func setupUser(container *libcontainer.Container) error { return nil } -// dupSlave dup2 the pty slave's fd into stdout and stdin and ensures that -// the slave's fd is 0, or stdin -func dupSlave(slave *os.File) error { - if err := system.Dup2(slave.Fd(), 0); err != nil { - return err - } - if err := system.Dup2(slave.Fd(), 1); err != nil { - return err - } - if err := system.Dup2(slave.Fd(), 2); err != nil { - return err - } - return nil -} - // setupVethNetwork uses the Network config if it is not nil to initialize // the new veth interface inside the container for use by changing the name to eth0 // setting the MTU and IP address along with the default gateway diff --git a/pkg/libcontainer/nsinit/mount.go b/pkg/libcontainer/nsinit/mount.go deleted file mode 100644 index dd6b1c8a43..0000000000 --- a/pkg/libcontainer/nsinit/mount.go +++ /dev/null @@ -1,265 +0,0 @@ -// +build linux - -package nsinit - -import ( - "fmt" - "github.com/dotcloud/docker/pkg/label" - "github.com/dotcloud/docker/pkg/libcontainer" - "github.com/dotcloud/docker/pkg/system" - "io/ioutil" - "os" - "path/filepath" - "syscall" -) - -// default mount point flags -const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV - -// setupNewMountNamespace is used to initialize a new mount namespace for an new -// container in the rootfs that is specified. -// -// There is no need to unmount the new mounts because as soon as the mount namespace -// is no longer in use, the mounts will be removed automatically -func setupNewMountNamespace(rootfs string, bindMounts []libcontainer.Mount, console string, readonly, noPivotRoot bool, mountLabel string) error { - flag := syscall.MS_PRIVATE - if noPivotRoot { - flag = syscall.MS_SLAVE - } - if err := system.Mount("", "/", "", uintptr(flag|syscall.MS_REC), ""); err != nil { - return fmt.Errorf("mounting / as slave %s", err) - } - if err := system.Mount(rootfs, rootfs, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { - return fmt.Errorf("mouting %s as bind %s", rootfs, err) - } - if err := mountSystem(rootfs, mountLabel); err != nil { - return fmt.Errorf("mount system %s", err) - } - - for _, m := range bindMounts { - var ( - flags = syscall.MS_BIND | syscall.MS_REC - dest = filepath.Join(rootfs, m.Destination) - ) - if !m.Writable { - flags = flags | syscall.MS_RDONLY - } - if err := system.Mount(m.Source, dest, "bind", uintptr(flags), ""); err != nil { - return fmt.Errorf("mounting %s into %s %s", m.Source, dest, err) - } - if !m.Writable { - if err := system.Mount(m.Source, dest, "bind", uintptr(flags|syscall.MS_REMOUNT), ""); err != nil { - return fmt.Errorf("remounting %s into %s %s", m.Source, dest, err) - } - } - if m.Private { - if err := system.Mount("", dest, "none", uintptr(syscall.MS_PRIVATE), ""); err != nil { - return fmt.Errorf("mounting %s private %s", dest, err) - } - } - } - - if err := copyDevNodes(rootfs); err != nil { - return fmt.Errorf("copy dev nodes %s", err) - } - if err := setupPtmx(rootfs, console, mountLabel); err != nil { - return err - } - if err := system.Chdir(rootfs); err != nil { - return fmt.Errorf("chdir into %s %s", rootfs, err) - } - - if noPivotRoot { - if err := rootMsMove(rootfs); err != nil { - return err - } - } else { - if err := rootPivot(rootfs); err != nil { - return err - } - } - - if readonly { - if err := system.Mount("/", "/", "bind", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_REC, ""); err != nil { - return fmt.Errorf("mounting %s as readonly %s", rootfs, err) - } - } - - system.Umask(0022) - - return nil -} - -// use a pivot root to setup the rootfs -func rootPivot(rootfs string) error { - pivotDir, err := ioutil.TempDir(rootfs, ".pivot_root") - if err != nil { - return fmt.Errorf("can't create pivot_root dir %s", pivotDir, err) - } - if err := system.Pivotroot(rootfs, pivotDir); err != nil { - return fmt.Errorf("pivot_root %s", err) - } - if err := system.Chdir("/"); err != nil { - return fmt.Errorf("chdir / %s", err) - } - // path to pivot dir now changed, update - pivotDir = filepath.Join("/", filepath.Base(pivotDir)) - if err := system.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { - return fmt.Errorf("unmount pivot_root dir %s", err) - } - if err := os.Remove(pivotDir); err != nil { - return fmt.Errorf("remove pivot_root dir %s", err) - } - return nil -} - -// use MS_MOVE and chroot to setup the rootfs -func rootMsMove(rootfs string) error { - if err := system.Mount(rootfs, "/", "", syscall.MS_MOVE, ""); err != nil { - return fmt.Errorf("mount move %s into / %s", rootfs, err) - } - if err := system.Chroot("."); err != nil { - return fmt.Errorf("chroot . %s", err) - } - if err := system.Chdir("/"); err != nil { - return fmt.Errorf("chdir / %s", err) - } - return nil -} - -// copyDevNodes mknods the hosts devices so the new container has access to them -func copyDevNodes(rootfs string) error { - oldMask := system.Umask(0000) - defer system.Umask(oldMask) - - for _, node := range []string{ - "null", - "zero", - "full", - "random", - "urandom", - "tty", - } { - if err := copyDevNode(rootfs, node); err != nil { - return err - } - } - return nil -} - -func copyDevNode(rootfs, node string) error { - stat, err := os.Stat(filepath.Join("/dev", node)) - if err != nil { - return err - } - var ( - dest = filepath.Join(rootfs, "dev", node) - st = stat.Sys().(*syscall.Stat_t) - ) - if err := system.Mknod(dest, st.Mode, int(st.Rdev)); err != nil && !os.IsExist(err) { - return fmt.Errorf("copy %s %s", node, err) - } - return nil -} - -// setupConsole ensures that the container has a proper /dev/console setup -func setupConsole(rootfs, console string, mountLabel string) error { - oldMask := system.Umask(0000) - defer system.Umask(oldMask) - - stat, err := os.Stat(console) - if err != nil { - return fmt.Errorf("stat console %s %s", console, err) - } - var ( - st = stat.Sys().(*syscall.Stat_t) - dest = filepath.Join(rootfs, "dev/console") - ) - if err := os.Remove(dest); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("remove %s %s", dest, err) - } - if err := os.Chmod(console, 0600); err != nil { - return err - } - if err := os.Chown(console, 0, 0); err != nil { - return err - } - if err := system.Mknod(dest, (st.Mode&^07777)|0600, int(st.Rdev)); err != nil { - return fmt.Errorf("mknod %s %s", dest, err) - } - if err := label.SetFileLabel(console, mountLabel); err != nil { - return fmt.Errorf("SetFileLabel Failed %s %s", dest, err) - } - if err := system.Mount(console, dest, "bind", syscall.MS_BIND, ""); err != nil { - return fmt.Errorf("bind %s to %s %s", console, dest, err) - } - return nil -} - -// mountSystem sets up linux specific system mounts like sys, proc, shm, and devpts -// inside the mount namespace -func mountSystem(rootfs string, mountLabel string) error { - for _, m := range []struct { - source string - path string - device string - flags int - data string - }{ - {source: "proc", path: filepath.Join(rootfs, "proc"), device: "proc", flags: defaultMountFlags}, - {source: "sysfs", path: filepath.Join(rootfs, "sys"), device: "sysfs", flags: defaultMountFlags}, - {source: "shm", path: filepath.Join(rootfs, "dev", "shm"), device: "tmpfs", flags: defaultMountFlags, data: label.FormatMountLabel("mode=1777,size=65536k", mountLabel)}, - {source: "devpts", path: filepath.Join(rootfs, "dev", "pts"), device: "devpts", flags: syscall.MS_NOSUID | syscall.MS_NOEXEC, data: label.FormatMountLabel("newinstance,ptmxmode=0666,mode=620,gid=5", mountLabel)}, - } { - if err := os.MkdirAll(m.path, 0755); err != nil && !os.IsExist(err) { - return fmt.Errorf("mkdirall %s %s", m.path, err) - } - if err := system.Mount(m.source, m.path, m.device, uintptr(m.flags), m.data); err != nil { - return fmt.Errorf("mounting %s into %s %s", m.source, m.path, err) - } - } - return nil -} - -// setupPtmx adds a symlink to pts/ptmx for /dev/ptmx and -// finishes setting up /dev/console -func setupPtmx(rootfs, console string, mountLabel string) error { - ptmx := filepath.Join(rootfs, "dev/ptmx") - if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) { - return err - } - if err := os.Symlink("pts/ptmx", ptmx); err != nil { - return fmt.Errorf("symlink dev ptmx %s", err) - } - if console != "" { - if err := setupConsole(rootfs, console, mountLabel); err != nil { - return err - } - } - return nil -} - -// remountProc is used to detach and remount the proc filesystem -// commonly needed with running a new process inside an existing container -func remountProc() error { - if err := system.Unmount("/proc", syscall.MNT_DETACH); err != nil { - return err - } - if err := system.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil { - return err - } - return nil -} - -func remountSys() error { - if err := system.Unmount("/sys", syscall.MNT_DETACH); err != nil { - if err != syscall.EINVAL { - return err - } - } else { - if err := system.Mount("sysfs", "/sys", "sysfs", uintptr(defaultMountFlags), ""); err != nil { - return err - } - } - return nil -} diff --git a/pkg/libcontainer/capabilities/capabilities.go b/pkg/libcontainer/security/capabilities/capabilities.go similarity index 100% rename from pkg/libcontainer/capabilities/capabilities.go rename to pkg/libcontainer/security/capabilities/capabilities.go diff --git a/pkg/libcontainer/security/restrict/restrict.go b/pkg/libcontainer/security/restrict/restrict.go new file mode 100644 index 0000000000..291d6ca5dc --- /dev/null +++ b/pkg/libcontainer/security/restrict/restrict.go @@ -0,0 +1,51 @@ +package restrict + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + + "github.com/dotcloud/docker/pkg/system" +) + +const flags = syscall.MS_BIND | syscall.MS_REC | syscall.MS_RDONLY + +var restrictions = map[string]string{ + // dirs + "/proc/sys": "", + "/proc/irq": "", + "/proc/acpi": "", + + // files + "/proc/sysrq-trigger": "/dev/null", + "/proc/kcore": "/dev/null", +} + +// Restrict locks down access to many areas of proc +// by using the asumption that the user does not have mount caps to +// revert the changes made here +func Restrict(rootfs, empty string) error { + for dest, source := range restrictions { + dest = filepath.Join(rootfs, dest) + + // we don't have a "/dev/null" for dirs so have the requester pass a dir + // for us to bind mount + switch source { + case "": + source = empty + default: + source = filepath.Join(rootfs, source) + } + if err := system.Mount(source, dest, "bind", flags, ""); err != nil { + if os.IsNotExist(err) { + continue + } + return fmt.Errorf("unable to mount %s over %s %s", source, dest, err) + } + if err := system.Mount("", dest, "bind", flags|syscall.MS_REMOUNT, ""); err != nil { + return fmt.Errorf("unable to mount %s over %s %s", source, dest, err) + } + } + return nil +} diff --git a/pkg/libcontainer/types.go b/pkg/libcontainer/types.go index d4818c3ffe..ade3c32f1d 100644 --- a/pkg/libcontainer/types.go +++ b/pkg/libcontainer/types.go @@ -11,6 +11,26 @@ var ( ErrUnsupported = errors.New("Unsupported method") ) +type Mounts []Mount + +func (s Mounts) OfType(t string) Mounts { + out := Mounts{} + for _, m := range s { + if m.Type == t { + out = append(out, m) + } + } + return out +} + +type Mount struct { + Type string `json:"type,omitempty"` + Source string `json:"source,omitempty"` // Source path, in the host namespace + Destination string `json:"destination,omitempty"` // Destination path, in the container + Writable bool `json:"writable,omitempty"` + Private bool `json:"private,omitempty"` +} + // namespaceList is used to convert the libcontainer types // into the names of the files located in /proc//ns/* for // each namespace diff --git a/registry/registry.go b/registry/registry.go index 451f30f670..84fe337414 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -292,6 +292,25 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [ return nil, fmt.Errorf("Could not reach any registry endpoint") } +func buildEndpointsList(headers []string, indexEp string) ([]string, error) { + var endpoints []string + parsedUrl, err := url.Parse(indexEp) + if err != nil { + return nil, err + } + var urlScheme = parsedUrl.Scheme + // The Registry's URL scheme has to match the Index' + for _, ep := range headers { + epList := strings.Split(ep, ",") + for _, epListElement := range epList { + endpoints = append( + endpoints, + fmt.Sprintf("%s://%s/v1/", urlScheme, strings.TrimSpace(epListElement))) + } + } + return endpoints, nil +} + func (r *Registry) GetRepositoryData(remote string) (*RepositoryData, error) { indexEp := r.indexEndpoint repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote) @@ -327,11 +346,10 @@ func (r *Registry) GetRepositoryData(remote string) (*RepositoryData, error) { } var endpoints []string - var urlScheme = indexEp[:strings.Index(indexEp, ":")] if res.Header.Get("X-Docker-Endpoints") != "" { - // The Registry's URL scheme has to match the Index' - for _, ep := range res.Header["X-Docker-Endpoints"] { - endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", urlScheme, ep)) + endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp) + if err != nil { + return nil, err } } else { return nil, fmt.Errorf("Index response didn't contain any endpoints") @@ -560,7 +578,6 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat } var tokens, endpoints []string - var urlScheme = indexEp[:strings.Index(indexEp, ":")] if !validate { if res.StatusCode != 200 && res.StatusCode != 201 { errBody, err := ioutil.ReadAll(res.Body) @@ -577,9 +594,9 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat } if res.Header.Get("X-Docker-Endpoints") != "" { - // The Registry's URL scheme has to match the Index' - for _, ep := range res.Header["X-Docker-Endpoints"] { - endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", urlScheme, ep)) + endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp) + if err != nil { + return nil, err } } else { return nil, fmt.Errorf("Index response didn't contain any endpoints") diff --git a/registry/registry_mock_test.go b/registry/registry_mock_test.go index dd5da6bd50..6b00751318 100644 --- a/registry/registry_mock_test.go +++ b/registry/registry_mock_test.go @@ -291,7 +291,7 @@ func handlerUsers(w http.ResponseWriter, r *http.Request) { func handlerImages(w http.ResponseWriter, r *http.Request) { u, _ := url.Parse(testHttpServer.URL) - w.Header().Add("X-Docker-Endpoints", u.Host) + w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com")) w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())) if r.Method == "PUT" { if strings.HasSuffix(r.URL.Path, "images") { diff --git a/registry/registry_test.go b/registry/registry_test.go index cb56502fc1..2717d85761 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -1,7 +1,9 @@ package registry import ( + "fmt" "github.com/dotcloud/docker/utils" + "net/url" "strings" "testing" ) @@ -99,12 +101,23 @@ func TestGetRemoteTags(t *testing.T) { func TestGetRepositoryData(t *testing.T) { r := spawnTestRegistry(t) + parsedUrl, err := url.Parse(makeURL("/v1/")) + if err != nil { + t.Fatal(err) + } + host := "http://" + parsedUrl.Host + "/v1/" data, err := r.GetRepositoryData("foo42/bar") if err != nil { t.Fatal(err) } assertEqual(t, len(data.ImgList), 2, "Expected 2 images in ImgList") - assertEqual(t, len(data.Endpoints), 1, "Expected one endpoint in Endpoints") + assertEqual(t, len(data.Endpoints), 2, + fmt.Sprintf("Expected 2 endpoints in Endpoints, found %d instead", len(data.Endpoints))) + assertEqual(t, data.Endpoints[0], host, + fmt.Sprintf("Expected first endpoint to be %s but found %s instead", host, data.Endpoints[0])) + assertEqual(t, data.Endpoints[1], "http://test.example.com/v1/", + fmt.Sprintf("Expected first endpoint to be http://test.example.com/v1/ but found %s instead", data.Endpoints[1])) + } func TestPushImageJSONRegistry(t *testing.T) {