1
0
mirror of https://github.com/docker/docs.git synced 2026-11-05 00:14:17 +03:00
This commit is contained in:
Misty Stanley-Jones
2016-09-28 14:53:14 -07:00
1090 changed files with 332702 additions and 51 deletions

View File

@@ -1,11 +0,0 @@
## Current behavior
## Expected behavior
##### Version affected
##### Docs referenced / affected

11
.gitignore vendored
View File

@@ -1,11 +0,0 @@
*.egg-info
*.pyc
.coverage*
/.tox
/build
/coverage-html
/dist
/docs/_site
/venv
README.rst
compose/GITSHA

1
.godir Normal file
View File

@@ -0,0 +1 @@
github.com/docker/swarm

View File

@@ -1,29 +0,0 @@
sudo: required
language: python
matrix:
include:
- os: linux
services:
- docker
- os: osx
language: generic
install: ./script/travis/install
script:
- ./script/travis/ci
- ./script/travis/build-binary
before_deploy:
- "./script/travis/render-bintray-config.py < ./script/travis/bintray.json.tmpl > ./bintray.json"
deploy:
provider: bintray
user: docker-compose-roleuser
key: '$BINTRAY_API_KEY'
file: ./bintray.json
skip_cleanup: true
on:
all_branches: true

325
CHANGELOG.md Normal file
View File

@@ -0,0 +1,325 @@
# Changelog
## 1.2.5 (2016-08-18)
#### Scheduler
- Fix container rescheduling with overlay network
- Fix scheduler detail log improper effect when container name is empty
- Check unique container name on create and rename for Mesos cluster
#### Health check
- Refresh container status on health_status events
#### Doc
- Fix install-w-machine.md using docker-machine --swarm feature
## 1.2.4 (2016-07-28)
#### API
- New client interface in Swarm, to differentiate from Swarm mode in Docker 1.12
- Underlying HTTP client for API is created inside Swarm
- Update minimum Docker Engine version supported by Swarm to 1.8
- Additional error handling
- Code refactoring
#### Networking
- Fix concurrent map writes race condition
- Refresh single network when network event is emitted (performance improvement)
- Avoid network refresh when creating container (performance improvement)
#### Volumes
- Refresh single volume when volume event is emitted (performance improvement)
- Avoid volume refresh when creating container (performance improvement)
#### Events
- Support daemon events for Swarm
#### Test
- Fix leader election tests
- Fix rescheduling test
#### Mesos
- Fix double locking issue
#### Misc
- Handle systime difference between Swarm and Engines
- Add healthcheck information to CLI
- Fix `engine_reconnect` issue that led to reconnected engine being treated as new
## 1.2.3 (2016-05-25)
#### API
- Update `engine-api` vendoring (supports new functions and signatures)
- Fix registry auth bug for image pulls
## 1.2.2 (2016-05-05)
#### Cluster management
- Fix deadlock that causes Swarm to hang
## 1.2.1 (2016-05-03)
#### Scheduler
- Add containerslots filter to allow user to limit container number on a node
#### API
- Use engine-api to handle large number of API calls
- Update ContainerConfig to embed HostConfig and NetworkingConfig
- stop/restart/kill a non-existent container should return 500 rather than 404
- Return an error when assertion fails in hijack
- Return an error when Image Pull fails
- Fix image pull bug (wait until download finishes)
- Fix and document some api response status codes
- Add NodeID in docker info
- Support docker ps --filter by volume
#### Build
- Move dependencies to vendor/
- Update Image Pull to use docker/distribution package
- Convert docs Dockerfiles to use docs/base:oss
#### Test
- Fix api/ps tests
- Update api/stats test to prevent timeout on master branch
- Use --cpu-shares instead of -c in integration test
#### Misc
- Documentation clean up
- Close http response body to avoid potential memory leak
- Switch context.TODO() to context.Background() to enable context setting
## 1.2.0 (2016-04-13)
#### Scheduler
- Move rescheduling out of experimental
- Differentiate constraint errors from affinity errors
- Printing unsatisfiable constraints for container scheduling failure
- Enable rescheduling on master manager to prevent replica managers from rescheduling containers
- Output error when starting a rescheduled container fails, and when removing container fails at node recovery
- Validate cluster swarm.overcommit option
#### API
- Introduce engine-api client to Swarm
- Implement 'info' and 'version' with engine-api
- Use apiClient for some volume, network, image operations
- Print engine version in Info
- Fix swarm api response status code
- Support ps node filter
- Fix HostConfig for /start endpoint
- Print container 'created' state at ps
- Update dockerclient to get labels on volumes, networks, images
- Support private images, labels and other new flags in docker build
- Select apiClient version according to node docker version
#### Node management
- Prevent crash on channel double close
- Manager retries EventMonitoring on failure.
- Docker engine updates hostname/domainname
- Force inspect for containers in Restarting state.
- Increase max thread count to 50k to accommodate large cluster or heavy workload
- Force to validate min and max refresh interval to be positive
- Skip unstable tests from Docker bug 14203
- Fix race condition between node removal from discovery and scheduler
- Fix data race with node failureCount
- Display warning message if an engine has labels with "node=xxx"
#### Discovery
- Remove parameter which is not used in createDiscovery
- Fix Consul leader election failure on multi-server
#### Mesos integration
- Support rescind offer in swarm
- Update mesos tests
#### Misc
- Update golang version to 1.5.4
- Skip redundant endpoints in "network inspect"
- Validate duration flags:--delay, --timeout, --replication-ttl
- Fix image matching via id
- Make port 0 invalid as listening port
- Improve volume inspect test
- Add read lock for eventsHandler when only it is necessary
## 1.1.3 (2016-03-03)
- Fix missing HostConfig for rescheduled containers
- Fix TCP connections leak
- Support `docker run --net <node>/<network> ...`
- Fix CORS issue in the API
## 1.1.2 (2016-02-18)
- Fix regression with Docker Compose
## 1.1.1 (2016-02-17)
- Performance improvements around networking with Docker engine 1.10
- Fix reschedule issue regarding events
- Implement engine refresh backoff strategy for failing nodes
## 1.1.0 (2016-02-04)
#### Scheduler
- Add support for container rescheduling on node failure. (experimental)
- Use failureCount as a secondary health indicator
- Add swarm container create retry option
- Fixed the way soft affinities and handled
#### API
- Support private registry on docker run
- Expose error and last update time in docker info
- Sort images by Created
- Fix error when inspect on unhealthy node
- Prevent panic in filters when container has no name
- Add buildtime, kernelversion and experimental to API version
- Support docker update and new networking related flags in run & network create/connect/disconnect/ls
- Require `--all` on docker ps to display containers on unhealthy nodes
- Retry on docker events EOF
#### Node Management
- Add a random delay to avoid synchronized registration at swarm join
- Use engine connection error to fail engine fast
- Introduce pending state
#### Mesos integration
- Rename slave to agent
- Upgrade tests to use mesos 0.25
- Code refactors
- Improve debug output
- Enable checkpoint failover in FrameworkInfo
- Fix timeout when pulling images
- Add timeout to refuse offers
- Fix double start issue
#### Misc
- Fix license grant
- Documentation update
- Use discovery from docker/docker
## 1.0.1 (2015-12-09)
#### Scheduler
- Set labels for pending containers to fix scheduler failure
#### Discovery
- Increase default TTL and heartbeat values to reduce traffic to discovery
#### API
- Fix 'ps -a' panic issue
- Fix network connect/disconnect for overlay network
- Fix connection leak on TLS connections
- Fix CLI hang on events command
- Fix newline issue with events
- Improve OPTIONS handler
- Fix image digest
- Fix memoryswappiness default value
- Enable profiling for HTTP in debug mode
#### Node Update
- Provide options on swarm node update frequency
#### Mesos integration
- Change offers timeout default to prevent other frameworks starvation
- Improve error output for bad swarm mesos user
- Fix connection failure when using Mesos with ZooKeeper
#### Misc
- Update to Go 1.5.2
- Documentation update
## 1.0 (2015-10-13)
#### Scheduler
- Swarm is now pulling Images in parallel after the scheduling decision has been made, which mitigates the error happening occasionally with Docker pulls blocking the entire scheduler.
#### General stability
- The Node refresh loop process has been improved and does not yield to a panic when removing an Engine and trying to refresh the state at the same time.
- The refresh loop has been randomized to better handle huge scale scenarios (> 1000 nodes) where a *refresh burst* could occur and make the Manager unstable/fail.
- General improvements and fixes for the Mesos experimental backend for swarm.
#### Integration with libnetwork / Support for overlay networking
- It is now possible to use the new networking features with Swarm through the `network` sub-system. Commands like `docker network create`, `docker network attach` and `docker network ls` are now available through swarm.
#### Integration with Docker Volume Plugins
- You can now use the docker volume plugin subsystem available with `docker volume`.
#### Leader Election
- You can now specify the `--replication-ttl` flag to control how long it takes for Replicas to be notified of the Primary failure and take over the lead.
#### TLS
- This is now possible to use TLS with discovery for `consul` and `etcd` using the new `--discovery-opt` flag.
## 0.4 (2015-08-04)
#### Scheduler
- Reschedule with soft affinity when image cannot be pulled
#### Store
- Replace the store pkg by libKV
- Fixes about consul/etcd and zookeeper
#### API
- Fix docker push name matching
- Fix docker exec status code
- Fix docker pull status code
#### Docker Engine Compatibility
- Improve docker info with docker client 1.7.x
- Add SystemTime, http_proxy, https_proxy and no_proxy to docker info
#### Mesos integration
- Task creation timeout configurable
- Fix issue with hostname in library image
- Use 'docker_port' attribute if available
- Add support for random ports
#### Misc
- Add doc on leader election / high availability of swarm manager
- Lots of typos/improvements to the doc
- Switch to golang 1.4

236
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,236 @@
# Contributing to Swarm
Want to hack on Swarm? Awesome! Here are instructions to get you
started.
Swarm is a part of the [Docker](https://www.docker.com) project, and follows
the same rules and principles. If you're already familiar with the way
Docker does things, you'll feel right at home.
Otherwise, go read Docker's
[contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md),
[issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md),
[review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and
[branches and tags](https://github.com/docker/docker/blob/master/project/BRANCHES-AND-TAGS.md).
### Development Environment Setup
Swarm is written in [the Go programming language](http://golang.org) and manages its dependencies using [Godep](http://github.com/tools/godep). This guide will walk you through installing Go, forking the Swarm repo, building Swarm from source and contributing pull requests (PRs) back to the Swarm project.
#### Install git, Go and Godep
If you don't already have `git` installed, you should install it. For example, on Ubuntu:
```sh
sudo apt-get install git
```
You also need Go 1.5 or higher. Download Go from [https://golang.org/dl/](https://golang.org/dl/). To install on Linux:
```sh
tar xzvf go1.5.4.linux-amd64.tar.gz
sudo mv go /usr/local
```
> **Note**: On Ubuntu, do not use `apt-get` to install Go. Its repositories tend
> to include older versions of Go. Instead, install the latest Go manually using the
> instructions provided on the Go site.
Create a Go project directory in your home directory:
```sh
mkdir ~/gocode # any name is fine
```
Add these to your `.bashrc`:
```sh
export GO15VENDOREXPERIMENT=1 # only required for go 1.5.x
export GOROOT=/usr/local/go
export GOPATH=~/gocode
export PATH=$PATH:$GOPATH/bin:$GOROOT/bin
```
Close and reopen your terminal.
Install Godep:
```sh
go get github.com/tools/godep
```
Install golint:
```sh
go get github.com/golang/lint/golint
```
#### Fork the Swarm repo
You will create a Github fork of the Swarm repository. You will make changes to this fork. When your work is ready for review, open a pull request to contribute the work back to the main Swarm repo.
Go to the [`docker/swarm` repo](https://github.com/docker/swarm) in a browser and click the "Fork" button in the top right corner. You'll end up with your form of Swarm in `https://github.com/<username>/swarm`.
(Throughout this document, `<username>` refers to your Github username.)
#### Clone Swarm and set remotes
Now clone the main Swarm repo:
```sh
mkdir -p $GOPATH/src/github.com/docker
cd $GOPATH/src/github.com/docker
git clone https://github.com/docker/swarm.git
cd swarm
```
For the easiest contribution workflow, we will set up remotes as follows:
* `origin` is your personal fork
* `upstream` is `docker/swarm`
Run these commands:
```sh
git remote remove origin
git remote add origin https://github.com/<username>/swarm.git
git remote add upstream https://github.com/docker/swarm.git
git remote set-url --push upstream no-pushing
```
You can check your configuration like this:
```sh
$ git remote -v
origin https://github.com/<username>/swarm.git (fetch)
origin https://github.com/<username>/swarm.git (push)
upstream https://github.com/docker/swarm.git (fetch)
upstream no-pushing (push)
```
#### Pull `docker/swarm` changes into your fork
As the `docker/swarm` moves forward, it will accumulate commits that are not in your personal fork. To keep your fork up to date, you need to periodically rebase:
```sh
git fetch upstream master
git rebase upstream/master
git push -f origin master
```
Don't worry: fetching the upstream and rebasing will not overwrite your local changes.
### Build the Swarm binary
Build the binary, installing it to `$GOPATH/bin/swarm`:
```sh
cd $GOPATH/src/github.com/docker/swarm
go install .
```
Run the binary you just created:
```sh
$GOPATH/bin/swarm help
$GOPATH/bin/swarm create
$GOPATH/bin/swarm manage token://<cluster_id>
$GOPATH/bin/swarm join --addr=<node_ip:2375> token://<cluster_id>
```
#### Create a Swarm image for deployment
Swarm is distributed as a Docker image with a single `swarm` binary inside. To create the image:
```sh
docker build -t my_swarm .
```
Now you can run the same commands as above using the container:
```sh
docker run my_swarm help
docker run -d my_swarm join --addr=<node_ip:2375> token://<cluster_id>
docker run -d -p <manager_port>:2375 my_swarm manage token://<cluster_id>
```
For complete documentation on how to use Swarm, refer to the Swarm section of [docs.docker.com](http://docs.docker.com/).
### Run tests
To run unit tests:
```sh
go test -v -race `go list ./... | grep -v /vendor/`
```
To run integration tests:
```sh
./test/integration/run.sh
```
You can use this command to check if `*.bats` files are formatted correctly:
```sh
find test/ -type f \( -name "*.sh" -or -name "*.bash" -or -name "*.bats" \) -exec grep -Hn -e "^ " {} \;
```
You can use this command to check if `*.go` files are formatted correctly:
```sh
golint ./...
```
### Submit a pull request (PR)
Swarm welcomes your contributions back to the project! We follow the same contribution process and principles as other Docker projects. This tutorial describes the [Docker contribution workflow](https://docs.docker.com/opensource/workflow/make-a-contribution/).
In general, your contribution workflow for Swarm will look like this:
```sh
cd $GOPATH/src/github.com/docker/swarm
git checkout master # Make sure you have a clean master
git pull upstream master # ...before you create your branch.
git checkout -b issue-9999 # Create a branch for your work
vi README.md # Edit a file in your branch
git add README.md # Git add your changed files
git commit -s -m "Edited README" # Make a signed commit to your local branch
git push origin issue-9999 # Push your local branch to your fork
```
To open a PR, browse to your fork at `http://github.com/<username>/swarm` and select the `issue-9999` branch from "Branch" drop down list. Click the green "New Pull Request" button and submit your PR. Maintainers will review your PR and merge it or provide feedback on how to improve it.
### Revise your PR
You may receive suggested changes on your PR before it can be merged. If you do, make the changes in the same branch, then do something like:
```sh
git checkout issue-9999
git add <changed files>
git commit -s --amend --no-edit
git push -f origin issue-9999
```
Github will magically update the open PR to add these changes.
### Talk to us
The best way to chat with other Swarm developers is on freenode `#docker-swarm`. ([See here](https://docs.docker.com/opensource/get-help/) for IRC instructions.)
You can also find maintainers' email addresses in `MAINTAINERS`.
### Advanced: Adding New Dependencies
To make sure other will not miss dependencies you've added to Swarm, you'll need to call `godep save` to make changes to the config file, `Godep/Godeps.json`. An important thing is that `godep` will replace the config file by the dependency information it learnt from your local machine. This step will mess the upstream config. So, changes to `Godep/Godeps.json` must be performed with care.
```sh
$GOBIN/godep save ./...
$GOBIN/godep update <an updated package>
git diff # check what added or removed in Godep/Godeps.json
# then manually add missing dependencies
```
To make sure you newly added codes will make the build process happy, you can try building Swarm in the same way as defined in `Dockerfile`.
```sh
go install
```
Then you should find the `swarm` binary under the `$GOBIN` directory.
Happy hacking!

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:1.5.4-alpine
ARG GOOS
COPY . /go/src/github.com/docker/swarm
WORKDIR /go/src/github.com/docker/swarm
ENV GO15VENDOREXPERIMENT=1
RUN set -ex \
&& apk add --no-cache --virtual .build-deps \
git \
&& GOOS=$GOOS CGO_ENABLED=0 go install -v -a -tags netgo -installsuffix netgo -ldflags "-w -X github.com/docker/swarm/version.GITCOMMIT `git rev-parse --short HEAD` -X github.com/docker/swarm/version.BUILDTIME \"`date -u`\"" \
&& apk del .build-deps
ENV SWARM_HOST :2375
EXPOSE 2375
VOLUME $HOME/.swarm
ENTRYPOINT ["swarm"]
CMD ["--help"]

386
Godeps/Godeps.json generated Normal file
View File

@@ -0,0 +1,386 @@
{
"ImportPath": "github.com/docker/swarm",
"GoVersion": "go1.5",
"GodepVersion": "v74",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/Microsoft/go-winio",
"Rev": "c40bf24f405ab3cc8e1383542d474e813332de6d"
},
{
"ImportPath": "github.com/Sirupsen/logrus",
"Comment": "v0.8.7-53-g446d1c1",
"Rev": "446d1c146faa8ed3f4218f056fcd165f6bcfda81"
},
{
"ImportPath": "github.com/codegangsta/cli",
"Comment": "1.2.0-187-gc31a797",
"Rev": "c31a7975863e7810c92e2e288a9ab074f9a88f29"
},
{
"ImportPath": "github.com/coreos/etcd/client",
"Comment": "v2.3.0-alpha.0-430-g374b14e",
"Rev": "374b14e47189c249c069c9b3376cf5c36f286fa6"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/pathutil",
"Comment": "v2.3.0-alpha.0-430-g374b14e",
"Rev": "374b14e47189c249c069c9b3376cf5c36f286fa6"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/types",
"Comment": "v2.3.0-alpha.0-430-g374b14e",
"Rev": "374b14e47189c249c069c9b3376cf5c36f286fa6"
},
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
},
{
"ImportPath": "github.com/docker/distribution/digest",
"Comment": "v2.3.0-rc.2-191-gee8450f",
"Rev": "ee8450ff13538d829a9a7909da9b979764d56865"
},
{
"ImportPath": "github.com/docker/distribution/reference",
"Comment": "v2.3.0-rc.2-191-gee8450f",
"Rev": "ee8450ff13538d829a9a7909da9b979764d56865"
},
{
"ImportPath": "github.com/docker/docker/pkg/discovery",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/discovery/file",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/discovery/kv",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/discovery/nodes",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/ioutils",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/longpath",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/parsers/kernel",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/random",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/stringid",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/docker/pkg/version",
"Comment": "v1.4.1-10581-gb65fd8e",
"Rev": "b65fd8e879545e8c9b859ea9b6b825ac50c79e46"
},
{
"ImportPath": "github.com/docker/engine-api/client",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/client/transport",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/client/transport/cancellable",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/blkiodev",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/container",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/events",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/filters",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/network",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/registry",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/strslice",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/time",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/go-connections/nat",
"Comment": "v0.2.0",
"Rev": "34b5052da6b11e27f5f2e357b38b571ddddd3928"
},
{
"ImportPath": "github.com/docker/go-connections/sockets",
"Comment": "v0.2.0",
"Rev": "34b5052da6b11e27f5f2e357b38b571ddddd3928"
},
{
"ImportPath": "github.com/docker/go-connections/tlsconfig",
"Comment": "v0.2.0",
"Rev": "34b5052da6b11e27f5f2e357b38b571ddddd3928"
},
{
"ImportPath": "github.com/docker/go-units",
"Comment": "v0.1.0-21-g0bbddae",
"Rev": "0bbddae09c5a5419a8c6dcdd7ff90da3d450393b"
},
{
"ImportPath": "github.com/docker/leadership",
"Rev": "bfc7753dd48af19513b29deec23c364bf0f274eb"
},
{
"ImportPath": "github.com/docker/libkv",
"Comment": "v0.2.1",
"Rev": "aabc039ad04deb721e234f99cd1b4aa28ac71a40"
},
{
"ImportPath": "github.com/docker/libkv/store",
"Comment": "v0.2.1",
"Rev": "aabc039ad04deb721e234f99cd1b4aa28ac71a40"
},
{
"ImportPath": "github.com/docker/libkv/store/consul",
"Comment": "v0.2.1",
"Rev": "aabc039ad04deb721e234f99cd1b4aa28ac71a40"
},
{
"ImportPath": "github.com/docker/libkv/store/etcd",
"Comment": "v0.2.1",
"Rev": "aabc039ad04deb721e234f99cd1b4aa28ac71a40"
},
{
"ImportPath": "github.com/docker/libkv/store/zookeeper",
"Comment": "v0.2.1",
"Rev": "aabc039ad04deb721e234f99cd1b4aa28ac71a40"
},
{
"ImportPath": "github.com/gogo/protobuf/proto",
"Comment": "v0.1-113-g7b13315",
"Rev": "7b1331554dbe882cb3613ee8f1824a5583627963"
},
{
"ImportPath": "github.com/golang/glog",
"Rev": "fca8c8854093a154ff1eb580aae10276ad6b1b5f"
},
{
"ImportPath": "github.com/gorilla/context",
"Rev": "1c83b3eabd45b6d76072b66b746c20815fb2872d"
},
{
"ImportPath": "github.com/gorilla/mux",
"Rev": "26a6070f849969ba72b72256e9f14cf519751690"
},
{
"ImportPath": "github.com/hashicorp/consul/api",
"Comment": "v0.6.1-11-g562bf11",
"Rev": "562bf11e9ff784824f9c5fec0ad3609805e13a3d"
},
{
"ImportPath": "github.com/hashicorp/go-cleanhttp",
"Rev": "ce617e79981a8fff618bb643d155133a8f38db96"
},
{
"ImportPath": "github.com/hashicorp/serf/coordinate",
"Comment": "v0.7.0-1-g39c7c06",
"Rev": "39c7c06298b480560202bec00c2c77e974e88792"
},
{
"ImportPath": "github.com/mesos/mesos-go/auth",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/auth/callback",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/auth/sasl",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/auth/sasl/mech",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/auth/sasl/mech/crammd5",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/detector",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/detector/zoo",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/mesosproto",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/mesosutil",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/mesosutil/process",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/messenger",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/scheduler",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/mesos/mesos-go/upid",
"Comment": "v0.0.2-27-g657252e",
"Rev": "657252ec6fc27975cd86da9f708acd6f4bb1f9ac"
},
{
"ImportPath": "github.com/opencontainers/runc/libcontainer/user",
"Comment": "v0.0.8-45-g2c31154",
"Rev": "2c3115481ee1782ad687a9e0b4834f89533c2acf"
},
{
"ImportPath": "github.com/pborman/uuid",
"Rev": "dee7705ef7b324f27ceb85a121c61f2c2e8ce988"
},
{
"ImportPath": "github.com/pmezard/go-difflib/difflib",
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
},
{
"ImportPath": "github.com/samalba/dockerclient",
"Rev": "52fe1c9d696f36d894d2319f1717a383ebc46f19"
},
{
"ImportPath": "github.com/samalba/dockerclient/mockclient",
"Rev": "52fe1c9d696f36d894d2319f1717a383ebc46f19"
},
{
"ImportPath": "github.com/samalba/dockerclient/nopclient",
"Rev": "52fe1c9d696f36d894d2319f1717a383ebc46f19"
},
{
"ImportPath": "github.com/samuel/go-zookeeper/zk",
"Rev": "218e9c81c0dd8b3b18172b2bbfad92cc7d6db55f"
},
{
"ImportPath": "github.com/skarademir/naturalsort",
"Rev": "69a5d87bef620f77ee8508db30c846b3b84b111e"
},
{
"ImportPath": "github.com/stretchr/objx",
"Rev": "1a9d0bb9f541897e62256577b352fdbc1fb4fd94"
},
{
"ImportPath": "github.com/stretchr/testify/assert",
"Comment": "v1.1.3",
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
},
{
"ImportPath": "github.com/stretchr/testify/mock",
"Comment": "v1.1.3",
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
},
{
"ImportPath": "github.com/ugorji/go/codec",
"Rev": "646ae4a518c1c3be0739df898118d9bccf993858"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "4fd4a9fed55e5bdee4a89d6406c2eabe38b60300"
},
{
"ImportPath": "golang.org/x/net/proxy",
"Rev": "4fd4a9fed55e5bdee4a89d6406c2eabe38b60300"
},
{
"ImportPath": "golang.org/x/sys/unix",
"Rev": "833a04a10549a95dc34458c195cbad61bbb6cb4d"
},
{
"ImportPath": "github.com/docker/engine-api/types/reference",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/versions",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
},
{
"ImportPath": "github.com/docker/engine-api/types/swarm",
"Comment": "v0.3.1-236-gd505506",
"Rev": "d505506c67611e8364526339452c20d6d0babcf6"
}
]
}

5
Godeps/Readme generated Normal file
View File

@@ -0,0 +1,5 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

191
LICENSE.code Normal file
View File

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

425
LICENSE.docs Normal file
View File

@@ -0,0 +1,425 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses.
Notwithstanding, Creative Commons may elect to apply one of its public
licenses to material it publishes and in those instances will be
considered the "Licensor." Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the public
licenses.
Creative Commons may be contacted at creativecommons.org.

120
MAINTAINERS Normal file
View File

@@ -0,0 +1,120 @@
# Swarm maintainers file
#
# This file describes who runs the docker/swarm project and how.
# This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
people = [
"abronan",
"allencloud",
"aluzzardi",
"chanwit",
"dongluochen",
"jimmyxian",
"mhbauer",
"nishanttotla",
"vieux",
]
[Org."Docs maintainers"]
people = [
"sfsmithcha",
"sven",
"thajeztah",
]
[Org.Curators]
# The curators help ensure that incoming issues and pull requests are properly triaged and
# that our various contribution and reviewing processes are respected. With their knowledge of
# the repository activity, they can also guide contributors to relevant material or
# discussions.
#
# They are neither code nor docs reviewers, so they are never expected to merge. They can
# however:
# - close an issue or pull request when it's an exact duplicate
# - close an issue or pull request when it's inappropriate or off-topic
people = [
"amitshukla"
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.abronan]
Name = "Alexandre Beslic"
Email = "abronan@docker.com"
GitHub = "abronan"
[people.aluzzardi]
Name = "Andrea Luzzardi"
Email = "al@docker.com"
GitHub = "aluzzardi"
[people.allencloud]
Name = "Allen Sun"
Email = "allen.sun@daocloud.io"
GitHub = "allencloud"
[people.amitshukla]
Name = "Amit Shukla"
Email = "amit.shukla@docker.com"
GitHub = "amitshukla"
[people.chanwit]
Name = "Chanwit Kaewkasi"
Email = "chanwit@gmail.com"
GitHub = "chanwit"
[people.dongluochen]
Name = "Dongluo Chen"
Email = "dong@docker.com"
GitHub = "dongluochen"
[people.mhbauer]
Name = "Morgan Bauer"
Email = "mbauer@us.ibm.com"
GitHub = "mhbauer"
[people.nishanttotla]
Name = "Nishant Totla"
Email = "nishanttotla@gmail.com"
GitHub = "nishanttotla"
[people.sfsmithcha]
Name = "Charles Smith"
Email = "charles.smith@docker.com"
GitHub = "sfsmithcha"
[people.sven]
Name = "Sven Dowideit"
Email = "SvenDowideit@home.org.au"
GitHub = "SvenDowideit"
[people.thajeztah]
Name = "Sebastiaan van Stijn"
Email = "github@gone.nl"
GitHub = "thaJeztah"
[people.vieux]
Name = "Victor Vieux"
Email = "vieux@docker.com"
GitHub = "vieux"
[people.jimmyxian]
Name = "Xian Chaobo"
Email = "xianchaobo@huawei.com"
GitHub = "jimmyxian"

79
README.md Normal file
View File

@@ -0,0 +1,79 @@
# Swarm: a Docker-native clustering system
[![GoDoc](https://godoc.org/github.com/docker/swarm?status.png)](https://godoc.org/github.com/docker/swarm)
[![Jenkins Build Status](https://jenkins.dockerproject.org/view/Swarm/job/Swarm%20Master/badge/icon)](https://jenkins.dockerproject.org/view/Swarm/job/Swarm%20Master/)
[![Build Status](https://travis-ci.org/docker/swarm.svg?branch=master)](https://travis-ci.org/docker/swarm)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/swarm)](https://goreportcard.com/report/github.com/docker/swarm)
![Docker Swarm Logo](logo.png?raw=true "Docker Swarm Logo")
Docker Swarm is native clustering for Docker. It turns a pool of Docker hosts
into a single, virtual host.
## Swarm Disambiguation
**Docker Swarm standalone**: This project. A native clustering system for
Docker. It turns a pool of Docker hosts into a single, virtual host using an
API proxy system. See [Docker Swarm overview](https://docs.docker.com/swarm/overview/).
It is Docker's first container orchestration project that began in 2014.
Combined with Docker Compose, it's a very convenient tool to schedule containers.
Its flexibility and simplicity make it easy to integrate with existing IT infrastructure.
Many companies and users have deployed Docker Swarm standalone for production and experimental
projects. Docker does not currently have a plan to deprecate Docker Swarm.
The Docker API is backward compatible so Docker Swarm will continue to work with
future Docker Engine versions.
**[Swarmkit](https://github.com/docker/swarmkit)**: Cluster
management and orchestration features in Docker Engine 1.12 or later. When Swarmkit
is enabled we call Docker Engine running in swarm mode. See the
feature list: [Swarm mode overview](https://docs.docker.com/engine/swarm/).
This project focuses on micro-service architecture. It supports service
reconciliation, load balancing, service discovery, built-in certificate rotation, etc.
Swarm mode is Docker's response to the community's request to simplify service orchestration.
While the 2 projects may accomplish similar tasks, they work on different levels
in terms of service architecture. Users can choose which one is more suitable for their workload.
If you're in doubt, Docker recommends that you try Docker 1.12 and later with built-in swarm mode.
## Docker Swarm standalone
Swarm serves the standard Docker API, so any tool which already communicates
with a Docker daemon can use Swarm to transparently scale to multiple hosts:
Dokku, Compose, Krane, Flynn, Deis, DockerUI, Shipyard, Drone, Jenkins... and,
of course, the Docker client itself.
Like other Docker projects, Swarm follows the "batteries included but removable"
principle. It ships with a set of simple scheduling backends out of the box, and
as initial development settles, an API will be developed to enable pluggable
backends. The goal is to provide a smooth out-of-the-box experience for simple
use cases, and allow swapping in more powerful backends, like Mesos, for large
scale production deployments.
## Installation for Swarm Users
For instructions on using Swarm in your dev, test or production environment, refer to the Docker Swarm documentation on [docs.docker.com](http://docs.docker.com/swarm/).
## Building Swarm from Source
To compile Swarm from source code, refer to the instructions in [CONTRIBUTING.md](http://github.com/docker/swarm/blob/master/CONTRIBUTING.md)
## Participating
You can contribute to Docker Swarm in several different ways:
- If you have comments, questions, or want to use your knowledge to help others, come join the conversation on IRC. You can reach us at #docker-swarm on Freenode.
- To report a problem or request a feature, please file an issue.
- Of course, we welcome pull requests and patches. Setting up a local Swarm development environment and submitting PRs is described [here](http://github.com/docker/swarm/blob/master/CONTRIBUTING.md).
Finally, if you want to see what we have for the future and learn more about our release cycles, all this information is detailed on the [wiki](https://github.com/docker/swarm/wiki)
## Copyright and license
Copyright © 2014-2016 Docker, Inc. All rights reserved, except as follows. Code
is released under the Apache 2.0 license. The README.md file, and files in the
"docs" folder are licensed under the Creative Commons Attribution 4.0
International License under the terms and conditions set forth in the file
"LICENSE.docs". You may obtain a duplicate copy of the same license, titled
CC-BY-SA-4.0, at http://creativecommons.org/licenses/by/4.0/.

76
RELEASE-CHECKLIST.md Normal file
View File

@@ -0,0 +1,76 @@
# Release Checklist
### 1. Update version and CHANGELOG.md on docker/swarm
```
git checkout -b bump-<version>
edit version/version.go
edit CHANGELOG.md
git add .
git commit -s -m "Bump version to <version>"
git push $GITHUBUSER bump-<version>
```
Open PR on docker/swarm. Merge the PR before moving to next step. If the release is an RC, then `CHANGELOG.md` should list the latest RC.
### 2. Rebase release branch on top of updated master branch and tag
```
git checkout master
git pull origin
git checkout release
git rebase master
git push origin
git tag <tag>
git push origin <tag>
```
The tag must be of the form `v1.2.1-rc1`.
### 3. Update library image
If you don't have `github.com:docker/swarm-library-image.git` repo, clone it.
```
git clone git@github.com:docker/swarm-library-image.git
```
Build and update Swarm image.
```
cd swarm-library-image
git pull
./update.sh <tag> (example: ./update.sh v0.2.0-rc2)
check build is successful (swarm binaries should show in git diff)
git add .
git commit -s -m “<tag>"
git push origin
```
### 4. Update official image
If you don't have https://github.com/docker-library/official-images.git, fork it.
```
git clone https://github.com/docker-library/official-images.git
cd official-images
git remote add $GITHUBUSER git@github.com:$GITHUBUSER/official-images.git
```
Update Swarm image list. You may add a new RC or a stable image to the image list. `latest` is for
latest stable image, normally an RC doesn't qualify.
```
git checkout master
git pull
git checkout -b update_swarm_<tag>
edit library/swarm
git add library/swarm
git commit -s -m "update swarm <tag>"
git push $GITHUBUSER update_swarm_<tag>
```
Open PR on docker-library/official-images.
### 5. Create release on github
Go to https://github.com/docker/swarm/releases/new use <tag> and changelog

50
ROADMAP.md Normal file
View File

@@ -0,0 +1,50 @@
Docker Swarm Roadmap
=====================
####Scheduler
Swarm comes with a builtin scheduler. It currently provides basic functionalities, such as
scheduling containers based on constraints or affinity (co-scheduling of containers), persistent
storage and multiple scheduling strategies like binpacking or random.
We plan to add more features to the builtin scheduler such as rebalancing (in case of host failure)
and global scheduling (schedule containers on every node)
* [ ] Virtual Container ID
* [ ] Rebalancing
* [ ] Global scheduling
####Leader Election (Distributed State)
Regarding Swarm Multi-tenancy, we are working on shared states between multiple "soon-to-be-master"
and master election.
* [ ] Shared state
* [ ] Master election
####API Matching
Swarm currently supports around 75% of the Docker API as you can see [here](https://github.com/docker/swarm/blob/master/api/README.md)
Our goal is to support 100% of the API, so all the Docker CLI commands would work against Swarm
* [ ] Bring Swarm API on par with Docker API as much as possible
* [ ] Use labels instead of env variable for constraints and affinity
* [ ] Open a few PRs on docker/docker to display labels in `docker ps` and `docker images`
####Networking
Scheduling and container placing is only part of the problem. In order for Swarm to be a good solution for distributed environments, we need to support multi-host networking.
* [ ] Integrate Swarm with [Docker Networking](https://github.com/docker/libnetwork).
####Extensibility
The builtin scheduler allows you to manage container on approximately hundreds of machines within a cluster.
In the future, we want to allow third party tools to be plugged into Docker Swarm to allow management
of much bigger clusters.
The first integration will be Mesos but other tools will arrive after, like Kubernetes.
* [x] Cluster Drivers [#393](https://github.com/docker/swarm/issues/393)
* [ ] Mesos implementation
Project Planning
================
An [Open-Source Planning Process](https://github.com/docker/swarm/wiki/Open-Source-Planning-Process) is used to define the Roadmap. [Project Pages](https://github.com/docker/swarm/wiki) define the goals for each Milestone and identify current progress.

8
api/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Docker Swarm API README
The Docker Swarm API is mostly compatible with the [Docker Remote
API](https://docs.docker.com/engine/reference/api/docker_remote_api/). To read
the end-user API documentation, visit [the Swarm API documentation on
docs.docker.com](https://docs.docker.com/swarm/swarm-api/). If you want to
modify the discovery API documentation, start with [the `docs/swarm-api.md`
file](../docs/swarm-api.md) in this project.

144
api/events.go Normal file
View File

@@ -0,0 +1,144 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/docker/swarm/cluster"
)
// EventsHandler broadcasts events to multiple client listeners.
type eventsHandler struct {
sync.RWMutex
ws map[string]io.Writer
cs map[string]chan struct{}
}
// NewEventsHandler creates a new EventsHandler for a cluster.
// The new eventsHandler is initialized with no writers or channels.
func newEventsHandler() *eventsHandler {
return &eventsHandler{
ws: make(map[string]io.Writer),
cs: make(map[string]chan struct{}),
}
}
// Add adds the writer and a new channel for the remote address.
func (eh *eventsHandler) Add(remoteAddr string, w io.Writer) {
eh.Lock()
eh.ws[remoteAddr] = w
eh.cs[remoteAddr] = make(chan struct{})
eh.Unlock()
}
// Wait waits on a signal from the remote address.
func (eh *eventsHandler) Wait(remoteAddr string, until int64) {
timer := time.NewTimer(0)
timer.Stop()
if until > 0 {
dur := time.Unix(until, 0).Sub(time.Now())
timer = time.NewTimer(dur)
}
// subscribe to http client close event
w := eh.ws[remoteAddr]
var closeNotify <-chan bool
if closeNotifier, ok := w.(http.CloseNotifier); ok {
closeNotify = closeNotifier.CloseNotify()
}
select {
case <-eh.cs[remoteAddr]:
case <-closeNotify:
case <-timer.C: // `--until` timeout
close(eh.cs[remoteAddr])
}
eh.cleanupHandler(remoteAddr)
}
func (eh *eventsHandler) cleanupHandler(remoteAddr string) {
eh.Lock()
// the maps are expected to have the same keys
delete(eh.cs, remoteAddr)
delete(eh.ws, remoteAddr)
eh.Unlock()
}
// Handle writes information about a cluster event to each remote address in the cluster that has been added to the events handler.
// After an unsuccessful write to a remote address, the associated channel is closed and the address is removed from the events handler.
func (eh *eventsHandler) Handle(e *cluster.Event) error {
// remove this hack once 1.10 is broadly adopted
from := e.From
e.From = e.From + " node:" + e.Engine.Name
// Attributes will be nil if the event was sent by engine < 1.10
if e.Actor.Attributes == nil {
e.Actor.Attributes = make(map[string]string)
}
e.Actor.Attributes["node.name"] = e.Engine.Name
e.Actor.Attributes["node.id"] = e.Engine.ID
e.Actor.Attributes["node.addr"] = e.Engine.Addr
e.Actor.Attributes["node.ip"] = e.Engine.IP
data, err := json.Marshal(e)
e.From = from
if err != nil {
return err
}
// remove the node field once 1.10 is broadly adopted & interlock stop relying on it
node := fmt.Sprintf(",%q:{%q:%q,%q:%q,%q:%q,%q:%q}}",
"node",
"Name", e.Engine.Name,
"Id", e.Engine.ID,
"Addr", e.Engine.Addr,
"Ip", e.Engine.IP,
)
// insert Node field
data = data[:len(data)-1]
data = append(data, []byte(node)...)
var failed []string
eh.RLock()
for key, w := range eh.ws {
if _, err := fmt.Fprintf(w, string(data)); err != nil {
// collect them to handle later under Lock
failed = append(failed, key)
continue
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
eh.RUnlock()
eh.Lock()
if len(failed) > 0 {
for _, key := range failed {
if ch, ok := eh.cs[key]; ok {
close(ch)
}
delete(eh.cs, key)
}
}
eh.Unlock()
return nil
}
// Size returns the number of remote addresses that the events handler currently contains.
func (eh *eventsHandler) Size() int {
eh.RLock()
defer eh.RUnlock()
return len(eh.ws)
}

69
api/events_test.go Normal file
View File

@@ -0,0 +1,69 @@
package api
import (
"encoding/json"
"fmt"
"testing"
"github.com/docker/swarm/cluster"
"github.com/stretchr/testify/assert"
)
type FakeWriter struct {
Tmp []byte
}
func (fw *FakeWriter) Write(p []byte) (n int, err error) {
fw.Tmp = append(fw.Tmp, p...)
return len(p), nil
}
func TestHandle(t *testing.T) {
eh := newEventsHandler()
assert.Equal(t, eh.Size(), 0)
fw := &FakeWriter{Tmp: []byte{}}
eh.Add("test", fw)
assert.Equal(t, eh.Size(), 1)
event := &cluster.Event{
Engine: &cluster.Engine{
ID: "node_id",
Name: "node_name",
IP: "node_ip",
Addr: "node_addr",
},
}
event.Message.Status = "status"
event.Message.ID = "id"
event.Message.From = "from"
event.Message.Time = 0
event.Actor.Attributes = make(map[string]string)
event.Actor.Attributes["nodevent.name"] = event.Engine.Name
event.Actor.Attributes["nodevent.id"] = event.Engine.ID
event.Actor.Attributes["nodevent.addr"] = event.Engine.Addr
event.Actor.Attributes["nodevent.ip"] = event.Engine.IP
assert.NoError(t, eh.Handle(event))
event.Message.From = "from node:node_name"
data, err := json.Marshal(event)
assert.NoError(t, err)
node := fmt.Sprintf(",%q:{%q:%q,%q:%q,%q:%q,%q:%q}}",
"node",
"Name", event.Engine.Name,
"Id", event.Engine.ID,
"Addr", event.Engine.Addr,
"Ip", event.Engine.IP,
)
// insert Node field
data = data[:len(data)-1]
data = append(data, []byte(node)...)
assert.Equal(t, string(data), string(fw.Tmp))
}

43
api/flusher.go Normal file
View File

@@ -0,0 +1,43 @@
package api
import (
"io"
"net/http"
"sync"
"github.com/docker/docker/pkg/ioutils"
)
// A WriteFlusher provides synchronized write access to the writer's underlying data stream and ensures that each write is flushed immediately.
type WriteFlusher struct {
sync.Mutex
w io.Writer
flusher http.Flusher
}
// Write writes the bytes to a stream and flushes the stream.
func (wf *WriteFlusher) Write(b []byte) (n int, err error) {
wf.Lock()
defer wf.Unlock()
n, err = wf.w.Write(b)
wf.flusher.Flush()
return n, err
}
// Flush flushes the stream immediately.
func (wf *WriteFlusher) Flush() {
wf.Lock()
defer wf.Unlock()
wf.flusher.Flush()
}
// NewWriteFlusher creates a new WriteFlusher for the writer.
func NewWriteFlusher(w io.Writer) *WriteFlusher {
var flusher http.Flusher
if f, ok := w.(http.Flusher); ok {
flusher = f
} else {
flusher = &ioutils.NopFlusher{}
}
return &WriteFlusher{w: w, flusher: flusher}
}

1373
api/handlers.go Normal file

File diff suppressed because it is too large Load Diff

406
api/mockclient/mock.go Normal file
View File

@@ -0,0 +1,406 @@
package mockclient
import (
"io"
"time"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/filters"
"github.com/docker/engine-api/types/network"
"github.com/docker/engine-api/types/registry"
"github.com/stretchr/testify/mock"
"golang.org/x/net/context"
)
// MockClient is a mock API Client based on engine-api
type MockClient struct {
mock.Mock
}
// NewMockClient creates a new mock client
func NewMockClient() *MockClient {
return &MockClient{}
}
// ClientVersion returns the version string associated with this instance of the Client
func (client *MockClient) ClientVersion() string {
args := client.Mock.Called()
return args.String(0)
}
// CheckpointCreate creates a checkpoint from the given container with the given name
func (client *MockClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
args := client.Mock.Called(ctx, container, options)
return args.Error(0)
}
// CheckpointDelete deletes the checkpoint with the given name from the given container
func (client *MockClient) CheckpointDelete(ctx context.Context, container string, checkpointID string) error {
args := client.Mock.Called(ctx, container, checkpointID)
return args.Error(0)
}
// CheckpointList returns the volumes configured in the docker host
func (client *MockClient) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
args := client.Mock.Called(ctx, container)
return args.Get(0).([]types.Checkpoint), args.Error(1)
}
// ContainerAttach attaches a connection to a container in the server
func (client *MockClient) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
args := client.Mock.Called(ctx, container, options)
return args.Get(0).(types.HijackedResponse), args.Error(1)
}
// ContainerCommit applies changes into a container and creates a new tagged image
func (client *MockClient) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
args := client.Mock.Called(ctx, container, options)
return args.Get(0).(types.ContainerCommitResponse), args.Error(1)
}
// ContainerCreate creates a new container based in the given configuration
func (client *MockClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
args := client.Mock.Called(ctx, config, hostConfig, networkingConfig, containerName)
return args.Get(0).(types.ContainerCreateResponse), args.Error(1)
}
// ContainerDiff shows differences in a container filesystem since it was started
func (client *MockClient) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) {
args := client.Mock.Called(ctx, container)
return args.Get(0).([]types.ContainerChange), args.Error(1)
}
// ContainerExecAttach attaches a connection to an exec process in the server
func (client *MockClient) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
args := client.Mock.Called(ctx, execID, config)
return args.Get(0).(types.HijackedResponse), args.Error(1)
}
// ContainerExecCreate creates a new exec configuration to run an exec process
func (client *MockClient) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
args := client.Mock.Called(ctx, container, config)
return args.Get(0).(types.ContainerExecCreateResponse), args.Error(1)
}
// ContainerExecInspect returns information about a specific exec process on the docker host
func (client *MockClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
args := client.Mock.Called(ctx, execID)
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
}
// ContainerExecResize changes the size of the tty for an exec process running inside a container
func (client *MockClient) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
args := client.Mock.Called(ctx, execID, options)
return args.Error(0)
}
// ContainerExecStart starts an exec process already create in the docker host
func (client *MockClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
args := client.Mock.Called(ctx, execID, config)
return args.Error(0)
}
// ContainerExport retrieves the raw contents of a container and returns them as an io.ReadCloser
func (client *MockClient) ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, container)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ContainerInspect returns the container information
func (client *MockClient) ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) {
args := client.Mock.Called(ctx, container)
return args.Get(0).(types.ContainerJSON), args.Error(1)
}
// ContainerInspectWithRaw returns the container information and its raw representation
func (client *MockClient) ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error) {
args := client.Mock.Called(ctx, container, getSize)
return args.Get(0).(types.ContainerJSON), args.Get(1).([]byte), args.Error(2)
}
// ContainerKill terminates the container process but does not remove the container from the docker host
func (client *MockClient) ContainerKill(ctx context.Context, container, signal string) error {
args := client.Mock.Called(ctx, container, signal)
return args.Error(0)
}
// ContainerList returns the list of containers in the docker host
func (client *MockClient) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
args := client.Mock.Called(ctx, options)
return args.Get(0).([]types.Container), args.Error(1)
}
// ContainerLogs returns the logs generated by a container in an io.ReadCloser
func (client *MockClient) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, container, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ContainerPause pauses the main process of a given container without terminating it
func (client *MockClient) ContainerPause(ctx context.Context, container string) error {
args := client.Mock.Called(ctx, container)
return args.Error(0)
}
// ContainerRemove kills and removes a container from the docker host
func (client *MockClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error {
args := client.Mock.Called(ctx, container, options)
return args.Error(0)
}
// ContainerRename changes the name of a given container
func (client *MockClient) ContainerRename(ctx context.Context, container, newContainerName string) error {
args := client.Mock.Called(ctx, container, newContainerName)
return args.Error(0)
}
// ContainerResize changes the size of the tty for a container
func (client *MockClient) ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error {
args := client.Mock.Called(ctx, container, options)
return args.Error(0)
}
// ContainerRestart stops and starts a container again
func (client *MockClient) ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error {
args := client.Mock.Called(ctx, container, timeout)
return args.Error(0)
}
// ContainerStatPath returns Stat information about a path inside the container filesystem
func (client *MockClient) ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) {
args := client.Mock.Called(ctx, container, path)
return args.Get(0).(types.ContainerPathStat), args.Error(1)
}
// ContainerStats returns near realtime stats for a given container
func (client *MockClient) ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, container, stream)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ContainerStart sends a request to the docker daemon to start a container
func (client *MockClient) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error {
args := client.Mock.Called(ctx, container, options)
return args.Error(0)
}
// ContainerStop stops a container without terminating the process
func (client *MockClient) ContainerStop(ctx context.Context, container string, timeout *time.Duration) error {
args := client.Mock.Called(ctx, container, timeout)
return args.Error(0)
}
// ContainerTop shows process information from within a container
func (client *MockClient) ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) {
args := client.Mock.Called(ctx, container, arguments)
return args.Get(0).(types.ContainerProcessList), args.Error(1)
}
// ContainerUnpause resumes the process execution within a container
func (client *MockClient) ContainerUnpause(ctx context.Context, container string) error {
args := client.Mock.Called(ctx, container)
return args.Error(0)
}
// ContainerUpdate updates resources of a container
func (client *MockClient) ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) error {
args := client.Mock.Called(ctx, container, updateConfig)
return args.Error(0)
}
// ContainerWait pauses execution until a container exits
func (client *MockClient) ContainerWait(ctx context.Context, container string) (int, error) {
args := client.Mock.Called(ctx, container)
return args.Int(0), args.Error(1)
}
// CopyFromContainer gets the content from the container and returns it as a Reader to manipulate it in the host
func (client *MockClient) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
args := client.Mock.Called(ctx, container, srcPath)
return args.Get(0).(io.ReadCloser), args.Get(1).(types.ContainerPathStat), args.Error(2)
}
// CopyToContainer copies content into the container filesystem
func (client *MockClient) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
args := client.Mock.Called(ctx, container, path, content, options)
return args.Error(0)
}
// Events returns a stream of events in the daemon in a ReadCloser
func (client *MockClient) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImageBuild sends request to the daemon to build images
func (client *MockClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
args := client.Mock.Called(ctx, context, options)
return args.Get(0).(types.ImageBuildResponse), args.Error(1)
}
// ImageCreate creates a new image based in the parent options
func (client *MockClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, parentReference, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImageHistory returns the changes in an image in history format
func (client *MockClient) ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) {
args := client.Mock.Called(ctx, image)
return args.Get(0).([]types.ImageHistory), args.Error(1)
}
// ImageImport creates a new image based in the source options
func (client *MockClient) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, source, ref, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImageInspectWithRaw returns the image information and it's raw representation
func (client *MockClient) ImageInspectWithRaw(ctx context.Context, image string, getSize bool) (types.ImageInspect, []byte, error) {
args := client.Mock.Called(ctx, image, getSize)
return args.Get(0).(types.ImageInspect), args.Get(1).([]byte), args.Error(2)
}
// ImageList returns a list of images in the docker host
func (client *MockClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) {
args := client.Mock.Called(ctx, options)
return args.Get(0).([]types.Image), args.Error(1)
}
// ImageLoad loads an image in the docker host from the client host
func (client *MockClient) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
args := client.Mock.Called(ctx, input, quiet)
return args.Get(0).(types.ImageLoadResponse), args.Error(1)
}
// ImagePull requests the docker host to pull an image from a remote registry
func (client *MockClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, ref, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImagePush requests the docker host to push an image to a remote registry
func (client *MockClient) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, ref, options)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImageRemove removes an image from the docker host
func (client *MockClient) ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) {
args := client.Mock.Called(ctx, image, options)
return args.Get(0).([]types.ImageDelete), args.Error(1)
}
// ImageSearch makes the docker host to search by a term in a remote registry
func (client *MockClient) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
args := client.Mock.Called(ctx, term, options)
return args.Get(0).([]registry.SearchResult), args.Error(1)
}
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser
func (client *MockClient) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) {
args := client.Mock.Called(ctx, images)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// ImageTag tags an image in the docker host
func (client *MockClient) ImageTag(ctx context.Context, image, ref string) error {
args := client.Mock.Called(ctx, image, ref)
return args.Error(0)
}
// Info returns information about the docker server
func (client *MockClient) Info(ctx context.Context) (types.Info, error) {
args := client.Mock.Called(ctx)
return args.Get(0).(types.Info), args.Error(1)
}
// NetworkConnect connects a container to an existent network in the docker host
func (client *MockClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
args := client.Mock.Called(ctx, networkID, container, config)
return args.Error(0)
}
// NetworkCreate creates a new network in the docker host
func (client *MockClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
args := client.Mock.Called(ctx, name, options)
return args.Get(0).(types.NetworkCreateResponse), args.Error(1)
}
// NetworkDisconnect disconnects a container from an existent network in the docker host
func (client *MockClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error {
args := client.Mock.Called(ctx, networkID, container, force)
return args.Error(0)
}
// NetworkInspect returns the information for a specific network configured in the docker host
func (client *MockClient) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) {
args := client.Mock.Called(ctx, networkID)
return args.Get(0).(types.NetworkResource), args.Error(1)
}
// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and it's raw representation
func (client *MockClient) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) {
args := client.Mock.Called(ctx, networkID)
return args.Get(0).(types.NetworkResource), args.Get(1).([]byte), args.Error(2)
}
// NetworkList returns the list of networks configured in the docker host
func (client *MockClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
args := client.Mock.Called(ctx, options)
return args.Get(0).([]types.NetworkResource), args.Error(1)
}
// NetworkRemove removes an existent network from the docker host
func (client *MockClient) NetworkRemove(ctx context.Context, networkID string) error {
args := client.Mock.Called(ctx, networkID)
return args.Error(0)
}
// RegistryLogin authenticates the docker server with a given docker registry
func (client *MockClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) {
args := client.Mock.Called(ctx, auth)
return args.Get(0).(types.AuthResponse), args.Error(1)
}
// ServerVersion returns information of the docker client and server host
func (client *MockClient) ServerVersion(ctx context.Context) (types.Version, error) {
args := client.Mock.Called(ctx)
return args.Get(0).(types.Version), args.Error(1)
}
// UpdateClientVersion updates the client version
func (client *MockClient) UpdateClientVersion(v string) {
}
// VolumeCreate creates a volume in the docker host
func (client *MockClient) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) {
args := client.Mock.Called(ctx, options)
return args.Get(0).(types.Volume), args.Error(1)
}
// VolumeInspect returns the information about a specific volume in the docker host
func (client *MockClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) {
args := client.Mock.Called(ctx, volumeID)
return args.Get(0).(types.Volume), args.Error(1)
}
// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation
func (client *MockClient) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) {
args := client.Mock.Called(ctx, volumeID)
return args.Get(0).(types.Volume), args.Get(1).([]byte), args.Error(2)
}
// VolumeList returns the volumes configured in the docker host
func (client *MockClient) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) {
args := client.Mock.Called(ctx, filter)
return args.Get(0).(types.VolumesListResponse), args.Error(1)
}
// VolumeRemove removes a volume from the docker host
func (client *MockClient) VolumeRemove(ctx context.Context, volumeID string) error {
args := client.Mock.Called(ctx, volumeID)
return args.Error(0)
}

View File

@@ -0,0 +1,36 @@
package mockclient
import (
"reflect"
"testing"
"golang.org/x/net/context"
"github.com/docker/engine-api/types"
"github.com/docker/swarm/swarmclient"
"github.com/stretchr/testify/mock"
)
func TestMock(t *testing.T) {
mockClient := NewMockClient()
mockClient.On("ServerVersion", mock.Anything).Return(types.Version{Version: "foo"}, nil).Once()
v, err := mockClient.ServerVersion(context.Background())
if err != nil {
t.Fatal(err)
}
if v.Version != "foo" {
t.Fatal(v)
}
mockClient.Mock.AssertExpectations(t)
}
func TestMockInterface(t *testing.T) {
iface := reflect.TypeOf((*swarmclient.SwarmAPIClient)(nil)).Elem()
mockClient := NewMockClient()
if !reflect.TypeOf(mockClient).Implements(iface) {
t.Fatalf("Mock does not implement the SwarmAPIClient interface")
}
}

347
api/nopclient/nop.go Normal file
View File

@@ -0,0 +1,347 @@
package nopclient
import (
"errors"
"io"
"time"
"golang.org/x/net/context"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/filters"
"github.com/docker/engine-api/types/network"
"github.com/docker/engine-api/types/registry"
)
var (
errNoEngine = errors.New("Engine no longer exists")
)
// NopClient is a nop API Client based on engine-api
type NopClient struct {
}
// NewNopClient creates a new nop client
func NewNopClient() *NopClient {
return &NopClient{}
}
// ClientVersion returns the version string associated with this instance of the Client
func (client *NopClient) ClientVersion() string {
return ""
}
// CheckpointCreate creates a checkpoint from the given container with the given name
func (client *NopClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
return errNoEngine
}
// CheckpointDelete deletes the checkpoint with the given name from the given container
func (client *NopClient) CheckpointDelete(ctx context.Context, container string, checkpointID string) error {
return errNoEngine
}
// CheckpointList returns the volumes configured in the docker host
func (client *NopClient) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
return nil, errNoEngine
}
// ContainerAttach attaches a connection to a container in the server
func (client *NopClient) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
return types.HijackedResponse{}, errNoEngine
}
// ContainerCommit applies changes into a container and creates a new tagged image
func (client *NopClient) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
return types.ContainerCommitResponse{}, errNoEngine
}
// ContainerCreate creates a new container based in the given configuration
func (client *NopClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
return types.ContainerCreateResponse{}, errNoEngine
}
// ContainerDiff shows differences in a container filesystem since it was started
func (client *NopClient) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) {
return nil, errNoEngine
}
// ContainerExecAttach attaches a connection to an exec process in the server
func (client *NopClient) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
return types.HijackedResponse{}, errNoEngine
}
// ContainerExecCreate creates a new exec configuration to run an exec process
func (client *NopClient) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
return types.ContainerExecCreateResponse{}, errNoEngine
}
// ContainerExecInspect returns information about a specific exec process on the docker host
func (client *NopClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
return types.ContainerExecInspect{}, errNoEngine
}
// ContainerExecResize changes the size of the tty for an exec process running inside a container
func (client *NopClient) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
return errNoEngine
}
// ContainerExecStart starts an exec process already create in the docker host
func (client *NopClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return errNoEngine
}
// ContainerExport retrieves the raw contents of a container and returns them as an io.ReadCloser
func (client *NopClient) ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerInspect returns the container information
func (client *NopClient) ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errNoEngine
}
// ContainerInspectWithRaw returns the container information and its raw representation
func (client *NopClient) ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error) {
return types.ContainerJSON{}, nil, errNoEngine
}
// ContainerKill terminates the container process but does not remove the container from the docker host
func (client *NopClient) ContainerKill(ctx context.Context, container, signal string) error {
return errNoEngine
}
// ContainerList returns the list of containers in the docker host
func (client *NopClient) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
return nil, errNoEngine
}
// ContainerLogs returns the logs generated by a container in an io.ReadCloser
func (client *NopClient) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerPause pauses the main process of a given container without terminating it
func (client *NopClient) ContainerPause(ctx context.Context, container string) error {
return errNoEngine
}
// ContainerRemove kills and removes a container from the docker host
func (client *NopClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error {
return errNoEngine
}
// ContainerRename changes the name of a given container
func (client *NopClient) ContainerRename(ctx context.Context, container, newContainerName string) error {
return errNoEngine
}
// ContainerResize changes the size of the tty for a container
func (client *NopClient) ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error {
return errNoEngine
}
// ContainerRestart stops and starts a container again
func (client *NopClient) ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error {
return errNoEngine
}
// ContainerStatPath returns Stat information about a path inside the container filesystem
func (client *NopClient) ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) {
return types.ContainerPathStat{}, errNoEngine
}
// ContainerStats returns near realtime stats for a given container
func (client *NopClient) ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerStart sends a request to the docker daemon to start a container
func (client *NopClient) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error {
return errNoEngine
}
// ContainerStop stops a container without terminating the process
func (client *NopClient) ContainerStop(ctx context.Context, container string, timeout *time.Duration) error {
return errNoEngine
}
// ContainerTop shows process information from within a container
func (client *NopClient) ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) {
return types.ContainerProcessList{}, errNoEngine
}
// ContainerUnpause resumes the process execution within a container
func (client *NopClient) ContainerUnpause(ctx context.Context, container string) error {
return errNoEngine
}
// ContainerUpdate updates resources of a container
func (client *NopClient) ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) error {
return errNoEngine
}
// ContainerWait pauses execution until a container exits
func (client *NopClient) ContainerWait(ctx context.Context, container string) (int, error) {
return 0, errNoEngine
}
// CopyFromContainer gets the content from the container and returns it as a Reader to manipulate it in the host
func (client *NopClient) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
return nil, types.ContainerPathStat{}, errNoEngine
}
// CopyToContainer copies content into the container filesystem
func (client *NopClient) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
return errNoEngine
}
// Events returns a stream of events in the daemon in a ReadCloser
func (client *NopClient) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageBuild sends request to the daemon to build images
func (client *NopClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
return types.ImageBuildResponse{}, errNoEngine
}
// ImageCreate creates a new image based in the parent options
func (client *NopClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageHistory returns the changes in an image in history format
func (client *NopClient) ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) {
return nil, errNoEngine
}
// ImageImport creates a new image based in the source options
func (client *NopClient) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageInspectWithRaw returns the image information and it's raw representation
func (client *NopClient) ImageInspectWithRaw(ctx context.Context, image string, getSize bool) (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, errNoEngine
}
// ImageList returns a list of images in the docker host
func (client *NopClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) {
return nil, errNoEngine
}
// ImageLoad loads an image in the docker host from the client host
func (client *NopClient) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
return types.ImageLoadResponse{}, errNoEngine
}
// ImagePull requests the docker host to pull an image from a remote registry
func (client *NopClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImagePush requests the docker host to push an image to a remote registry
func (client *NopClient) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageRemove removes an image from the docker host
func (client *NopClient) ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) {
return nil, errNoEngine
}
// ImageSearch makes the docker host to search by a term in a remote registry
func (client *NopClient) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
return nil, errNoEngine
}
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser
func (client *NopClient) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageTag tags an image in the docker host
func (client *NopClient) ImageTag(ctx context.Context, image, ref string) error {
return errNoEngine
}
// Info returns information about the docker server
func (client *NopClient) Info(ctx context.Context) (types.Info, error) {
return types.Info{}, errNoEngine
}
// NetworkConnect connects a container to an existent network in the docker host
func (client *NopClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
return errNoEngine
}
// NetworkCreate creates a new network in the docker host
func (client *NopClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
return types.NetworkCreateResponse{}, errNoEngine
}
// NetworkDisconnect disconnects a container from an existent network in the docker host
func (client *NopClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error {
return errNoEngine
}
// NetworkInspect returns the information for a specific network configured in the docker host
func (client *NopClient) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) {
return types.NetworkResource{}, errNoEngine
}
// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and it's raw representation
func (client *NopClient) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) {
return types.NetworkResource{}, nil, errNoEngine
}
// NetworkList returns the list of networks configured in the docker host
func (client *NopClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return nil, errNoEngine
}
// NetworkRemove removes an existent network from the docker host
func (client *NopClient) NetworkRemove(ctx context.Context, networkID string) error {
return errNoEngine
}
// RegistryLogin authenticates the docker server with a given docker registry
func (client *NopClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) {
return types.AuthResponse{}, errNoEngine
}
// ServerVersion returns information of the docker client and server host
func (client *NopClient) ServerVersion(ctx context.Context) (types.Version, error) {
return types.Version{}, errNoEngine
}
// UpdateClientVersion updates the client version
func (client *NopClient) UpdateClientVersion(v string) {
}
// VolumeCreate creates a volume in the docker host
func (client *NopClient) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) {
return types.Volume{}, errNoEngine
}
// VolumeInspect returns the information about a specific volume in the docker host
func (client *NopClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) {
return types.Volume{}, errNoEngine
}
// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation
func (client *NopClient) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) {
return types.Volume{}, nil, errNoEngine
}
// VolumeList returns the volumes configured in the docker host
func (client *NopClient) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) {
return types.VolumesListResponse{}, errNoEngine
}
// VolumeRemove removes a volume from the docker host
func (client *NopClient) VolumeRemove(ctx context.Context, volumeID string) error {
return errNoEngine
}

26
api/nopclient/nop_test.go Normal file
View File

@@ -0,0 +1,26 @@
package nopclient
import (
"reflect"
"testing"
"github.com/docker/swarm/swarmclient"
"golang.org/x/net/context"
)
func TestNop(t *testing.T) {
nop := NewNopClient()
_, err := nop.Info(context.Background())
if err != errNoEngine {
t.Fatalf("Nop client did not return error")
}
}
func TestNopInterface(t *testing.T) {
iface := reflect.TypeOf((*swarmclient.SwarmAPIClient)(nil)).Elem()
nop := NewNopClient()
if !reflect.TypeOf(nop).Implements(iface) {
t.Fatalf("Nop does not implement the SwarmAPIClient interface")
}
}

182
api/primary.go Normal file
View File

@@ -0,0 +1,182 @@
package api
import (
"crypto/tls"
"net/http"
"net/http/pprof"
log "github.com/Sirupsen/logrus"
"github.com/docker/swarm/cluster"
"github.com/gorilla/mux"
)
// Primary router context, used by handlers.
type context struct {
cluster cluster.Cluster
eventsHandler *eventsHandler
statusHandler StatusHandler
debug bool
tlsConfig *tls.Config
apiVersion string
}
type handler func(c *context, w http.ResponseWriter, r *http.Request)
var routes = map[string]map[string]handler{
"HEAD": {
"/containers/{name:.*}/archive": proxyContainer,
},
"GET": {
"/_ping": ping,
"/events": getEvents,
"/info": getInfo,
"/version": getVersion,
"/images/json": getImagesJSON,
"/images/viz": notImplementedHandler,
"/images/search": proxyRandom,
"/images/get": getImages,
"/images/{name:.*}/get": proxyImageGet,
"/images/{name:.*}/history": proxyImage,
"/images/{name:.*}/json": proxyImage,
"/containers/ps": getContainersJSON,
"/containers/json": getContainersJSON,
"/containers/{name:.*}/archive": proxyContainer,
"/containers/{name:.*}/export": proxyContainer,
"/containers/{name:.*}/changes": proxyContainer,
"/containers/{name:.*}/json": getContainerJSON,
"/containers/{name:.*}/top": proxyContainer,
"/containers/{name:.*}/logs": proxyContainer,
"/containers/{name:.*}/stats": proxyContainer,
"/containers/{name:.*}/attach/ws": proxyHijack,
"/exec/{execid:.*}/json": proxyContainer,
"/networks": getNetworks,
"/networks/{networkid:.*}": getNetwork,
"/volumes": getVolumes,
"/volumes/{volumename:.*}": getVolume,
},
"POST": {
"/auth": proxyRandom,
"/commit": postCommit,
"/build": postBuild,
"/images/create": postImagesCreate,
"/images/load": postImagesLoad,
"/images/{name:.*}/push": proxyImagePush,
"/images/{name:.*}/tag": postTagImage,
"/containers/create": postContainersCreate,
"/containers/{name:.*}/kill": proxyContainerAndForceRefresh,
"/containers/{name:.*}/pause": proxyContainerAndForceRefresh,
"/containers/{name:.*}/unpause": proxyContainerAndForceRefresh,
"/containers/{name:.*}/rename": postRenameContainer,
"/containers/{name:.*}/restart": proxyContainerAndForceRefresh,
"/containers/{name:.*}/start": postContainersStart,
"/containers/{name:.*}/stop": proxyContainerAndForceRefresh,
"/containers/{name:.*}/update": proxyContainerAndForceRefresh,
"/containers/{name:.*}/wait": proxyContainerAndForceRefresh,
"/containers/{name:.*}/resize": proxyContainer,
"/containers/{name:.*}/attach": proxyHijack,
"/containers/{name:.*}/copy": proxyContainer,
"/containers/{name:.*}/exec": postContainersExec,
"/exec/{execid:.*}/start": postExecStart,
"/exec/{execid:.*}/resize": proxyContainer,
"/networks/create": postNetworksCreate,
"/networks/{networkid:.*}/connect": proxyNetworkConnect,
"/networks/{networkid:.*}/disconnect": proxyNetworkDisconnect,
"/volumes/create": postVolumesCreate,
},
"PUT": {
"/containers/{name:.*}/archive": proxyContainer,
},
"DELETE": {
"/containers/{name:.*}": deleteContainers,
"/images/{name:.*}": deleteImages,
"/networks/{networkid:.*}": deleteNetworks,
"/volumes/{name:.*}": deleteVolumes,
},
}
func writeCorsHeaders(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD")
}
func profilerSetup(mainRouter *mux.Router, path string) {
var r = mainRouter.PathPrefix(path).Subrouter()
r.HandleFunc("/pprof/", pprof.Index)
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/pprof/profile", pprof.Profile)
r.HandleFunc("/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
r.HandleFunc("/pprof/block", pprof.Handler("block").ServeHTTP)
r.HandleFunc("/pprof/heap", pprof.Handler("heap").ServeHTTP)
r.HandleFunc("/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP)
r.HandleFunc("/pprof/threadcreate", pprof.Handler("threadcreate").ServeHTTP)
}
// NewPrimary creates a new API router.
func NewPrimary(cluster cluster.Cluster, tlsConfig *tls.Config, status StatusHandler, debug, enableCors bool) *mux.Router {
// Register the API events handler in the cluster.
eventsHandler := newEventsHandler()
cluster.RegisterEventHandler(eventsHandler)
context := &context{
cluster: cluster,
eventsHandler: eventsHandler,
statusHandler: status,
tlsConfig: tlsConfig,
}
r := mux.NewRouter()
setupPrimaryRouter(r, context, enableCors)
if debug {
profilerSetup(r, "/debug/")
}
return r
}
func setupPrimaryRouter(r *mux.Router, context *context, enableCors bool) {
for method, mappings := range routes {
for route, fct := range mappings {
log.WithFields(log.Fields{"method": method, "route": route}).Debug("Registering HTTP route")
localRoute := route
localFct := fct
wrap := func(w http.ResponseWriter, r *http.Request) {
log.WithFields(log.Fields{"method": r.Method, "uri": r.RequestURI}).Debug("HTTP request received")
if enableCors {
writeCorsHeaders(w, r)
}
context.apiVersion = mux.Vars(r)["version"]
localFct(context, w, r)
}
localMethod := method
r.Path("/v{version:[0-9]+.[0-9]+}" + localRoute).Methods(localMethod).HandlerFunc(wrap)
r.Path(localRoute).Methods(localMethod).HandlerFunc(wrap)
if enableCors {
optionsMethod := "OPTIONS"
optionsFct := optionsHandler
wrap := func(w http.ResponseWriter, r *http.Request) {
log.WithFields(log.Fields{"method": optionsMethod, "uri": r.RequestURI}).
Debug("HTTP request received")
if enableCors {
writeCorsHeaders(w, r)
}
context.apiVersion = mux.Vars(r)["version"]
optionsFct(context, w, r)
}
r.Path("/v{version:[0-9]+.[0-9]+}" + localRoute).
Methods(optionsMethod).HandlerFunc(wrap)
r.Path(localRoute).Methods(optionsMethod).
HandlerFunc(wrap)
}
}
}
}

81
api/primary_test.go Normal file
View File

@@ -0,0 +1,81 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
)
func TestRequest(t *testing.T) {
t.Parallel()
context := &context{}
r := mux.NewRouter()
setupPrimaryRouter(r, context, false)
w := httptest.NewRecorder()
req, e := http.NewRequest("GET", "/version", nil)
if nil != e {
t.Fatalf("couldn't set up test request")
}
r.ServeHTTP(w, req)
t.Logf("%d - %s", w.Code, w.Body.String())
t.Log("Expecting no extra Headers")
for k, v := range w.Header() {
t.Log(k, " : ", v)
}
if w.Code == 404 {
t.Fatalf("failed not found")
}
}
func TestCorsRequest(t *testing.T) {
t.Parallel()
context := &context{}
primary := mux.NewRouter()
setupPrimaryRouter(primary, context, true)
w := httptest.NewRecorder()
// test an OPTIONS request when cors enabled
r, e := http.NewRequest("OPTIONS", "/version", nil)
if nil != e {
t.Fatalf("couldn't set up test request")
}
primary.ServeHTTP(w, r)
t.Logf("%d - %s", w.Code, w.Body.String())
t.Log("Expecting extra cors Headers")
for k, v := range w.Header() {
t.Log(k, " : ", v)
}
if w.Code == 404 {
t.Fatalf("failed not found")
}
// test a normal request ( GET /_ping ) when cors enabled
w2 := httptest.NewRecorder()
r2, e2 := http.NewRequest("GET", "/_ping", nil)
if nil != e2 {
t.Fatalf("couldn't set up test request")
}
primary.ServeHTTP(w2, r2)
if w2.Body.String() != "OK" {
t.Fatalf("couldn't get body content when cors enabled")
}
if w2.Code == 404 {
t.Fatalf("failed not found")
}
}

52
api/replica.go Normal file
View File

@@ -0,0 +1,52 @@
package api
import (
"crypto/tls"
"fmt"
"net/http"
"strings"
)
var localRoutes = []string{"/info", "/_ping", "/debug"}
// Replica is an API replica that reserves proxy to the primary.
type Replica struct {
handler http.Handler
tlsConfig *tls.Config
primary string
}
// NewReplica creates a new API replica.
func NewReplica(handler http.Handler, tlsConfig *tls.Config) *Replica {
return &Replica{
handler: handler,
tlsConfig: tlsConfig,
}
}
// SetPrimary sets the address of the primary Swarm manager
func (p *Replica) SetPrimary(primary string) {
// FIXME: We have to kill current connections before doing this.
p.primary = primary
}
// ServeHTTP is the http.Handler.
func (p *Replica) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check whether we should handle this request locally.
for _, route := range localRoutes {
if strings.HasSuffix(r.URL.Path, route) {
p.handler.ServeHTTP(w, r)
return
}
}
// Otherwise, forward.
if p.primary == "" {
httpError(w, "No elected primary cluster manager", http.StatusInternalServerError)
return
}
if err := hijack(p.tlsConfig, p.primary, w, r); err != nil {
httpError(w, fmt.Sprintf("Unable to reach primary cluster manager (%s): %v", err, p.primary), http.StatusInternalServerError)
}
}

124
api/server.go Normal file
View File

@@ -0,0 +1,124 @@
package api
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
log "github.com/Sirupsen/logrus"
)
// DefaultDockerPort is the default port to listen on for incoming connections.
const DefaultDockerPort = ":2375"
// Dispatcher is a meta http.Handler. It acts as an http.Handler and forwards
// requests to another http.Handler that can be changed at runtime.
type dispatcher struct {
handler http.Handler
}
// SetHandler changes the underlying handler.
func (d *dispatcher) SetHandler(handler http.Handler) {
d.handler = handler
}
// ServeHTTP forwards requests to the underlying handler.
func (d *dispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if d.handler == nil {
httpError(w, "No dispatcher defined", http.StatusInternalServerError)
return
}
d.handler.ServeHTTP(w, r)
}
// Server is a Docker API server.
type Server struct {
hosts []string
tlsConfig *tls.Config
dispatcher *dispatcher
}
// NewServer creates an api.Server.
func NewServer(hosts []string, tlsConfig *tls.Config) *Server {
return &Server{
hosts: hosts,
tlsConfig: tlsConfig,
dispatcher: &dispatcher{},
}
}
// SetHandler is used to overwrite the HTTP handler for the API.
// This can be the api router or a reverse proxy.
func (s *Server) SetHandler(handler http.Handler) {
s.dispatcher.SetHandler(handler)
}
func newListener(proto, addr string, tlsConfig *tls.Config) (net.Listener, error) {
l, err := net.Listen(proto, addr)
if err != nil {
if strings.Contains(err.Error(), "address already in use") && strings.Contains(addr, DefaultDockerPort) {
return nil, fmt.Errorf("%s: is Docker already running on this machine? Try using a different port", err)
}
return nil, err
}
if tlsConfig != nil {
tlsConfig.NextProtos = []string{"http/1.1"}
l = tls.NewListener(l, tlsConfig)
}
return l, nil
}
// ListenAndServe starts an HTTP server on each host to listen on its
// TCP or Unix network address and calls Serve on each host's server
// to handle requests on incoming connections.
//
// The expected format for a host string is [protocol://]address. The protocol
// must be either "tcp" or "unix", with "tcp" used by default if not specified.
func (s *Server) ListenAndServe() error {
chErrors := make(chan error, len(s.hosts))
for _, host := range s.hosts {
protoAddrParts := strings.SplitN(host, "://", 2)
if len(protoAddrParts) == 1 {
protoAddrParts = append([]string{"tcp"}, protoAddrParts...)
}
go func() {
log.WithFields(log.Fields{"proto": protoAddrParts[0], "addr": protoAddrParts[1]}).Info("Listening for HTTP")
var (
l net.Listener
err error
server = &http.Server{
Addr: protoAddrParts[1],
Handler: s.dispatcher,
}
)
switch protoAddrParts[0] {
case "unix":
l, err = newUnixListener(protoAddrParts[1], s.tlsConfig)
case "tcp":
l, err = newListener("tcp", protoAddrParts[1], s.tlsConfig)
default:
err = fmt.Errorf("unsupported protocol: %q", protoAddrParts[0])
}
if err != nil {
chErrors <- err
} else {
chErrors <- server.Serve(l)
}
}()
}
for i := 0; i < len(s.hosts); i++ {
err := <-chErrors
if err != nil {
return err
}
}
return nil
}

36
api/server_unix.go Normal file
View File

@@ -0,0 +1,36 @@
// +build !windows
package api
import (
"crypto/tls"
"net"
"os"
"syscall"
)
func newUnixListener(addr string, tlsConfig *tls.Config) (net.Listener, error) {
if err := syscall.Unlink(addr); err != nil && !os.IsNotExist(err) {
return nil, err
}
// there is no way to specify the unix rights to use when
// creating the socket with net.Listener, so we use umask
// to create the file without rights and then we chmod
// to the desired unix rights. This prevent unwanted
// connections between the creation and the chmod
mask := syscall.Umask(0777)
defer syscall.Umask(mask)
l, err := newListener("unix", addr, tlsConfig)
if err != nil {
return nil, err
}
// only usable by the user who started swarm
if err := os.Chmod(addr, 0600); err != nil {
return nil, err
}
return l, nil
}

13
api/server_windows.go Normal file
View File

@@ -0,0 +1,13 @@
// +build windows
package api
import (
"crypto/tls"
"fmt"
"net"
)
func newUnixListener(addr string, tlsConfig *tls.Config) (net.Listener, error) {
return nil, fmt.Errorf("Windows platform does not support a unix socket")
}

46
api/sorter.go Normal file
View File

@@ -0,0 +1,46 @@
package api
import (
"github.com/docker/engine-api/types"
"github.com/docker/swarm/cluster"
)
// ContainerSorter implements the Sort interface to sort Docker containers.
// It is not guaranteed to be a stable sort.
type ContainerSorter []*cluster.Container
// Len returns the number of containers to be sorted.
func (s ContainerSorter) Len() int {
return len(s)
}
// Swap exchanges the container elements with indices i and j.
func (s ContainerSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the container with index i should sort before the container with index j.
// Containers are sorted chronologically by when they were created.
func (s ContainerSorter) Less(i, j int) bool {
return s[i].Info.Created < s[j].Info.Created
}
// ImageSorter implements the Sort interface to sort Docker Images.
// It is not guaranteed to be a stable sort.
type ImageSorter []types.Image
// Len returns the number of images to be sorted.
func (s ImageSorter) Len() int {
return len(s)
}
// Swap exchanges the container elements with indices i and j.
func (s ImageSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the Image with index i should sort before the Image with index j.
// Images are sorted chronologically by when they were created.
func (s ImageSorter) Less(i, j int) bool {
return s[i].Created < s[j].Created
}

7
api/status.go Normal file
View File

@@ -0,0 +1,7 @@
package api
// StatusHandler allows the API to display extra information on docker info.
type StatusHandler interface {
// Info provides key/values to be added to docker info.
Status() [][2]string
}

336
api/utils.go Normal file
View File

@@ -0,0 +1,336 @@
package api
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/swarm/cluster"
)
// Emit an HTTP error and log it.
func httpError(w http.ResponseWriter, err string, status int) {
log.WithField("status", status).Errorf("HTTP error: %v", err)
http.Error(w, err, status)
}
func sendJSONMessage(w io.Writer, id, status string) {
message := struct {
ID string `json:"id,omitempty"`
Status string `json:"status,omitempty"`
Progress interface{} `json:"progressDetail,omitempty"`
}{
id,
status,
struct{}{}, // this is required by the docker cli to have a proper display
}
json.NewEncoder(w).Encode(message)
}
func sendErrorJSONMessage(w io.Writer, errorCode int, errorMessage string) {
error := struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}{
errorCode,
errorMessage,
}
message := struct {
ErrorMsg string `json:"error,omitempty"`
Error interface{} `json:"errorDetail,omitempty"`
}{
errorMessage,
&error,
}
json.NewEncoder(w).Encode(message)
}
func getContainerFromVars(c *context, vars map[string]string) (string, *cluster.Container, error) {
if name, ok := vars["name"]; ok {
if container := c.cluster.Container(name); container != nil {
if !container.Engine.IsHealthy() {
return name, container, fmt.Errorf("Container %s running on unhealthy node %s", name, container.Engine.Name)
}
return name, container, nil
}
return name, nil, fmt.Errorf("No such container: %s", name)
}
if ID, ok := vars["execid"]; ok {
for _, container := range c.cluster.Containers() {
for _, execID := range container.Info.ExecIDs {
if ID == execID {
return "", container, nil
}
}
}
return "", nil, fmt.Errorf("Exec %s not found", ID)
}
return "", nil, errors.New("Not found")
}
// from https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go#L82
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func proxyAsync(engine *cluster.Engine, w http.ResponseWriter, r *http.Request, callback func(*http.Response)) error {
// RequestURI may not be sent to client
r.RequestURI = ""
client, scheme, err := engine.HTTPClientAndScheme()
if err != nil {
return err
}
r.URL.Scheme = scheme
r.URL.Host = engine.Addr
log.WithFields(log.Fields{"method": r.Method, "url": r.URL}).Debug("Proxy request")
resp, err := client.Do(r)
if err != nil {
return err
}
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(NewWriteFlusher(w), resp.Body)
if callback != nil {
callback(resp)
}
// cleanup
resp.Body.Close()
return nil
}
func proxy(engine *cluster.Engine, w http.ResponseWriter, r *http.Request) error {
return proxyAsync(engine, w, r, nil)
}
type tlsClientConn struct {
*tls.Conn
rawConn net.Conn
}
func (c *tlsClientConn) CloseWrite() error {
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
// on its underlying connection.
if cwc, ok := c.rawConn.(interface {
CloseWrite() error
}); ok {
log.Debug("Calling CloseWrite on Hijacked TLS Conn")
return cwc.CloseWrite()
}
return nil
}
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
// order to return our custom tlsClientCon struct which holds both the tls.Conn
// object _and_ its underlying raw connection. The rationale for this is that
// we need to be able to close the write end of the connection when attaching,
// which tls.Conn does not provide.
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
// We want the Timeout and Deadline values from dialer to cover the
// whole process: TCP connection and TLS handshake. This means that we
// also need to start our own timers now.
timeout := dialer.Timeout
if !dialer.Deadline.IsZero() {
deadlineTimeout := dialer.Deadline.Sub(time.Now())
if timeout == 0 || deadlineTimeout < timeout {
timeout = deadlineTimeout
}
}
var errChannel chan error
if timeout != 0 {
errChannel = make(chan error, 2)
time.AfterFunc(timeout, func() {
errChannel <- errors.New("")
})
}
rawConn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
colonPos := strings.LastIndex(addr, ":")
if colonPos == -1 {
colonPos = len(addr)
}
hostname := addr[:colonPos]
// If no ServerName is set, infer the ServerName
// from the hostname we're connecting to.
if config.ServerName == "" {
// Make a copy to avoid polluting argument or default.
c := *config
c.ServerName = hostname
config = &c
}
conn := tls.Client(rawConn, config)
if timeout == 0 {
err = conn.Handshake()
} else {
go func() {
errChannel <- conn.Handshake()
}()
err = <-errChannel
}
if err != nil {
rawConn.Close()
return nil, err
}
// This is Docker difference with standard's crypto/tls package: returned a
// wrapper which holds both the TLS and raw connections.
return &tlsClientConn{conn, rawConn}, nil
}
func dialHijack(tlsConfig *tls.Config, addr string) (net.Conn, error) {
if tlsConfig == nil {
return net.Dial("tcp", addr)
}
return tlsDialWithDialer(new(net.Dialer), "tcp", addr, tlsConfig)
}
func hijack(tlsConfig *tls.Config, addr string, w http.ResponseWriter, r *http.Request) error {
if parts := strings.SplitN(addr, "://", 2); len(parts) == 2 {
addr = parts[1]
}
log.WithField("addr", addr).Debug("Proxy hijack request")
var (
d net.Conn
err error
)
d, err = dialHijack(tlsConfig, addr)
if err != nil {
return err
}
hj, ok := w.(http.Hijacker)
if !ok {
return errors.New("Docker server does not support hijacking")
}
nc, _, err := hj.Hijack()
if err != nil {
return err
}
defer nc.Close()
defer d.Close()
err = r.Write(d)
if err != nil {
return err
}
cp := func(dst io.Writer, src io.Reader, chDone chan struct{}) {
io.Copy(dst, src)
if conn, ok := dst.(interface {
CloseWrite() error
}); ok {
conn.CloseWrite()
}
close(chDone)
}
inDone := make(chan struct{})
outDone := make(chan struct{})
go cp(d, nc, inDone)
go cp(nc, d, outDone)
// 1. When stdin is done, wait for stdout always
// 2. When stdout is done, close the stream and wait for stdin to finish
//
// On 2, stdin copy should return immediately now since the out stream is closed.
// Note that we probably don't actually even need to wait here.
//
// If we don't close the stream when stdout is done, in some cases stdin will hang
select {
case <-inDone:
// wait for out to be done
<-outDone
case <-outDone:
// close the conn and wait for stdin
nc.Close()
<-inDone
}
return nil
}
func boolValue(r *http.Request, k string) bool {
s := strings.ToLower(strings.TrimSpace(r.FormValue(k)))
return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none")
}
func intValueOrZero(r *http.Request, k string) int {
val, err := strconv.Atoi(r.FormValue(k))
if err != nil {
return 0
}
return val
}
func int64ValueOrZero(r *http.Request, k string) int64 {
val, err := strconv.ParseInt(r.FormValue(k), 10, 64)
if err != nil {
return 0
}
return val
}
func tagHasDigest(tag string) bool {
return strings.Contains(tag, ":")
}
// TODO(nishanttotla): There might be a better way to pass a ref string than construct it here
// getImageRef returns a string containing the registry reference given a repo and tag
func getImageRef(repo, tag string) string {
ref := repo
if tag != "" {
if tagHasDigest(tag) {
ref += "@" + tag
} else {
ref += ":" + tag
}
}
return ref
}

76
api/utils_test.go Normal file
View File

@@ -0,0 +1,76 @@
package api
import (
"net/http"
"net/url"
"testing"
)
func TestBoolValue(t *testing.T) {
cases := map[string]bool{
"": false,
"0": false,
"no": false,
"false": false,
"none": false,
"1": true,
"yes": true,
"true": true,
"one": true,
"100": true,
}
for c, e := range cases {
v := url.Values{}
v.Set("test", c)
r, _ := http.NewRequest("POST", "", nil)
r.Form = v
a := boolValue(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
}
}
func TestIntValueOrZero(t *testing.T) {
cases := map[string]int{
"": 0,
"asdf": 0,
"0": 0,
"1": 1,
}
for c, e := range cases {
v := url.Values{}
v.Set("test", c)
r, _ := http.NewRequest("POST", "", nil)
r.Form = v
a := intValueOrZero(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
}
}
func TestInt64ValueOrZero(t *testing.T) {
cases := map[string]int64{
"": 0,
"asdf": 0,
"0": 0,
"1": 1,
}
for c, e := range cases {
v := url.Values{}
v.Set("test", c)
r, _ := http.NewRequest("POST", "", nil)
r.Form = v
a := int64ValueOrZero(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
}
}

79
cli/cli.go Normal file
View File

@@ -0,0 +1,79 @@
package cli
import (
"fmt"
"os"
"path"
"runtime/debug"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/swarm/experimental"
"github.com/docker/swarm/version"
)
// Run the Swarm CLI.
func Run() {
setProgramLimits()
app := cli.NewApp()
app.Name = path.Base(os.Args[0])
app.Usage = "A Docker-native clustering system"
app.Version = version.VERSION + " (" + version.GITCOMMIT + ")"
app.Author = ""
app.Email = ""
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "debug mode",
EnvVar: "DEBUG",
},
cli.StringFlag{
Name: "log-level, l",
Value: "info",
Usage: fmt.Sprintf("Log level (options: debug, info, warn, error, fatal, panic)"),
},
cli.BoolFlag{
Name: "experimental",
Usage: "enable experimental features",
},
}
// logs
app.Before = func(c *cli.Context) error {
log.SetOutput(os.Stderr)
level, err := log.ParseLevel(c.String("log-level"))
if err != nil {
log.Fatalf(err.Error())
}
log.SetLevel(level)
// If a log level wasn't specified and we are running in debug mode,
// enforce log-level=debug.
if !c.IsSet("log-level") && !c.IsSet("l") && c.Bool("debug") {
log.SetLevel(log.DebugLevel)
}
experimental.ENABLED = c.Bool("experimental")
return nil
}
app.Commands = commands
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func setProgramLimits() {
// Swarm runnable threads could be large when the number of nodes is large
// or under request bursts. Most threads are occupied by network connections.
// Increase max thread count from 10k default to 50k to accommodate it.
const maxThreadCount int = 50 * 1000
debug.SetMaxThreads(maxThreadCount)
}

43
cli/commands.go Normal file
View File

@@ -0,0 +1,43 @@
package cli
import "github.com/codegangsta/cli"
var (
commands = []cli.Command{
{
Name: "create",
ShortName: "c",
Usage: "Create a cluster",
Action: create,
},
{
Name: "list",
ShortName: "l",
Usage: "List nodes in a cluster",
Flags: []cli.Flag{flTimeout, flDiscoveryOpt},
Action: list,
},
{
Name: "manage",
ShortName: "m",
Usage: "Manage a docker cluster",
Flags: []cli.Flag{
flStrategy, flFilter,
flHosts,
flLeaderElection, flLeaderTTL, flManageAdvertise,
flTLS, flTLSCaCert, flTLSCert, flTLSKey, flTLSVerify,
flRefreshIntervalMin, flRefreshIntervalMax, flFailureRetry, flRefreshRetry,
flHeartBeat,
flEnableCors,
flCluster, flDiscoveryOpt, flClusterOpt},
Action: manage,
},
{
Name: "join",
ShortName: "j",
Usage: "Join a docker cluster",
Flags: []cli.Flag{flJoinAdvertise, flHeartBeat, flTTL, flJoinRandomDelay, flDiscoveryOpt},
Action: join,
},
}
)

22
cli/create.go Normal file
View File

@@ -0,0 +1,22 @@
package cli
import (
"fmt"
"log"
"github.com/codegangsta/cli"
"github.com/docker/swarm/discovery/token"
)
func create(c *cli.Context) {
if len(c.Args()) != 0 {
log.Fatalf("the `create` command takes no arguments. See '%s create --help'.", c.App.Name)
}
discovery := &token.Discovery{}
discovery.Initialize("", 0, 0, nil)
token, err := discovery.CreateCluster()
if err != nil {
log.Fatal(err)
}
fmt.Println(token)
}

154
cli/flags.go Normal file
View File

@@ -0,0 +1,154 @@
package cli
import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/codegangsta/cli"
"github.com/docker/swarm/scheduler/filter"
"github.com/docker/swarm/scheduler/strategy"
)
func homepath(p string) string {
home := os.Getenv("HOME")
if runtime.GOOS == "windows" {
home = os.Getenv("USERPROFILE")
}
return filepath.Join(home, p)
}
func getDiscovery(c *cli.Context) string {
if len(c.Args()) == 1 {
return c.Args()[0]
}
return os.Getenv("SWARM_DISCOVERY")
}
var (
flJoinAdvertise = cli.StringFlag{
Name: "advertise, addr",
Usage: "Address of the Docker Engine joining the cluster. Swarm manager(s) MUST be able to reach the Docker Engine at this address.",
EnvVar: "SWARM_ADVERTISE",
}
flJoinRandomDelay = cli.StringFlag{
Name: "delay",
Value: "0s",
Usage: "add a random delay in [0s,delay] to avoid synchronized registration",
}
flManageAdvertise = cli.StringFlag{
Name: "advertise, addr",
Usage: "Address of the swarm manager joining the cluster. Other swarm manager(s) MUST be able to reach the swarm manager at this address.",
EnvVar: "SWARM_ADVERTISE",
}
// hack for go vet
flHostsValue = cli.StringSlice([]string{"tcp://127.0.0.1:2375"})
flHosts = cli.StringSliceFlag{
Name: "host, H",
Value: &flHostsValue,
Usage: "ip/socket to listen on",
EnvVar: "SWARM_HOST",
}
flHeartBeat = cli.StringFlag{
Name: "heartbeat",
Value: "60s",
Usage: "period between each heartbeat",
}
flTTL = cli.StringFlag{
Name: "ttl",
Value: "180s",
Usage: "set the expiration of an ephemeral node",
}
flTimeout = cli.StringFlag{
Name: "timeout",
Value: "10s",
Usage: "timeout period",
}
flRefreshIntervalMin = cli.StringFlag{
Name: "engine-refresh-min-interval",
Value: "30s",
Usage: "set engine refresh minimum interval",
}
flRefreshIntervalMax = cli.StringFlag{
Name: "engine-refresh-max-interval",
Value: "60s",
Usage: "set engine refresh maximum interval",
}
flRefreshRetry = cli.IntFlag{
Name: "engine-refresh-retry",
Value: 3,
Usage: "deprecated; replaced by --engine-failure-retry",
}
flFailureRetry = cli.IntFlag{
Name: "engine-failure-retry",
Value: 3,
Usage: "set engine failure retry count",
}
flEnableCors = cli.BoolFlag{
Name: "api-enable-cors, cors",
Usage: "enable CORS headers in the remote API",
}
flTLS = cli.BoolFlag{
Name: "tls",
Usage: "use TLS; implied by --tlsverify=true",
}
flTLSCaCert = cli.StringFlag{
Name: "tlscacert",
Usage: "trust only remotes providing a certificate signed by the CA given here",
}
flTLSCert = cli.StringFlag{
Name: "tlscert",
Usage: "path to TLS certificate file",
}
flTLSKey = cli.StringFlag{
Name: "tlskey",
Usage: "path to TLS key file",
}
flTLSVerify = cli.BoolFlag{
Name: "tlsverify",
Usage: "use TLS and verify the remote",
}
flStrategy = cli.StringFlag{
Name: "strategy",
Usage: "placement strategy to use [" + strings.Join(strategy.List(), ", ") + "]",
Value: strategy.List()[0],
}
// hack for go vet
flFilterValue = cli.StringSlice(filter.List())
// DefaultFilterNumber is exported
DefaultFilterNumber = len(flFilterValue)
flFilter = cli.StringSliceFlag{
Name: "filter, f",
Usage: "filter to use [" + strings.Join(filter.List(), ", ") + "]",
Value: &flFilterValue,
}
flCluster = cli.StringFlag{
Name: "cluster-driver, c",
Usage: "cluster driver to use [swarm, mesos-experimental]",
Value: "swarm",
}
flClusterOpt = cli.StringSliceFlag{
Name: "cluster-opt",
Usage: "cluster driver options",
Value: &cli.StringSlice{},
}
flDiscoveryOpt = cli.StringSliceFlag{
Name: "discovery-opt",
Usage: "discovery options",
Value: &cli.StringSlice{},
}
flLeaderElection = cli.BoolFlag{
Name: "replication",
Usage: "Enable Swarm manager replication",
}
flLeaderTTL = cli.StringFlag{
Name: "replication-ttl",
Value: "20s",
Usage: "Leader lock release time on failure",
}
)

57
cli/help.go Normal file
View File

@@ -0,0 +1,57 @@
package cli
import (
"os"
"path"
"github.com/codegangsta/cli"
)
func init() {
cli.AppHelpTemplate = `Usage: {{.Name}} {{if .Flags}}[OPTIONS] {{end}}COMMAND [arg...]
{{.Usage}}
Version: {{.Version}}{{if or .Author .Email}}
Author:{{if .Author}}
{{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}}
{{.Email}}{{end}}{{end}}
{{if .Flags}}
Options:
{{range .Flags}}{{.}}
{{end}}{{end}}
Commands:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
{{end}}
Run '{{.Name}} COMMAND --help' for more information on a command.
`
// See https://github.com/codegangsta/cli/pull/171/files
cli.CommandHelpTemplate = `{{$DISCOVERY := or (eq .Name "manage") (eq .Name "join") (eq .Name "list")}}Usage: ` + path.Base(os.Args[0]) + ` {{.Name}}{{if .Flags}} [OPTIONS]{{end}} {{if $DISCOVERY}}<discovery>{{end}}
{{.Usage}}{{if $DISCOVERY}}
Arguments:
<discovery> discovery service to use [$SWARM_DISCOVERY]
* token://<token>
* consul://<ip>/<path>
* etcd://<ip1>,<ip2>/<path>
* file://path/to/file
* zk://<ip1>,<ip2>/<path>
* [nodes://]<ip1>,<ip2>{{end}}{{if .Flags}}
Options:
{{range .Flags}}{{.}}
{{end}}{{if (eq .Name "manage")}}{{printf "\t * swarm.overcommit=0.05\tovercommit to apply on resources"}}
{{printf "\t * swarm.createretry=0\tcontainer create retry count after initial failure"}}
{{printf "\t * mesos.address=\taddress to bind on [$SWARM_MESOS_ADDRESS]"}}
{{printf "\t * mesos.checkpointfailover=false\tcheckpointing allows a restarted slave to reconnect with old executors and recover status updates, at the cost of disk I/O [$SWARM_MESOS_CHECKPOINT_FAILOVER]"}}
{{printf "\t * mesos.port=\tport to bind on [$SWARM_MESOS_PORT]"}}
{{printf "\t * mesos.offertimeout=30s\ttimeout for offers [$SWARM_MESOS_OFFER_TIMEOUT]"}}
{{printf "\t * mesos.offerrefusetimeout=5s\tseconds to consider unused resources refused [$SWARM_MESOS_OFFER_REFUSE_TIMEOUT]"}}
{{printf "\t * mesos.tasktimeout=5s\ttimeout for task creation [$SWARM_MESOS_TASK_TIMEOUT]"}}
{{printf "\t * mesos.user=\tframework user [$SWARM_MESOS_USER]"}}{{end}}{{ end }}
`
}

83
cli/join.go Normal file
View File

@@ -0,0 +1,83 @@
package cli
import (
"math/rand"
"net"
"strconv"
"time"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/discovery"
)
func checkAddrFormat(addr string) bool {
// validate addr is in host:port form. Use net function to handle both IPv4/IPv6 cases.
_, port, err := net.SplitHostPort(addr)
if err != nil {
return false
}
portNum, err := strconv.Atoi(port)
return err == nil && portNum > 0 && portNum <= 65535
}
func join(c *cli.Context) {
dflag := getDiscovery(c)
if dflag == "" {
log.Fatalf("discovery required to join a cluster. See '%s join --help'.", c.App.Name)
}
addr := c.String("advertise")
if addr == "" {
log.Fatal("missing mandatory --advertise flag")
}
if !checkAddrFormat(addr) {
log.Fatal("--advertise should be of the form ip:port or hostname:port")
}
joinDelay, err := time.ParseDuration(c.String("delay"))
if err != nil {
log.Fatalf("invalid --delay: %v", err)
}
if joinDelay < time.Duration(0)*time.Second {
log.Fatalf("--delay should not be a negative number")
}
hb, err := time.ParseDuration(c.String("heartbeat"))
if err != nil {
log.Fatalf("invalid --heartbeat: %v", err)
}
if hb < 1*time.Second {
log.Fatal("--heartbeat should be at least one second")
}
ttl, err := time.ParseDuration(c.String("ttl"))
if err != nil {
log.Fatalf("invalid --ttl: %v", err)
}
if ttl <= hb {
log.Fatal("--ttl must be strictly superior to the heartbeat value")
}
d, err := discovery.New(dflag, hb, ttl, getDiscoveryOpt(c))
if err != nil {
log.Fatal(err)
}
// if joinDelay is 0, no delay will be executed
// if joinDelay is larger than 0,
// add a random delay between 0s and joinDelay at start to avoid synchronized registration
if joinDelay > 0 {
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
delay := time.Duration(r.Int63n(int64(joinDelay)))
log.Infof("Add a random delay %s to avoid synchronized registration", delay)
time.Sleep(delay)
}
for {
log.WithFields(log.Fields{"addr": addr, "discovery": dflag}).Infof("Registering on the discovery service every %s...", hb)
if err := d.Register(addr); err != nil {
log.Error(err)
}
time.Sleep(hb)
}
}

36
cli/join_test.go Normal file
View File

@@ -0,0 +1,36 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheckAddrFormat(t *testing.T) {
assert.False(t, checkAddrFormat("1.1.1.1"))
assert.False(t, checkAddrFormat("hostname"))
assert.False(t, checkAddrFormat("1.1.1.1:"))
assert.False(t, checkAddrFormat("hostname:"))
assert.False(t, checkAddrFormat("1.1.1.1:111111"))
assert.False(t, checkAddrFormat("hostname:111111"))
assert.False(t, checkAddrFormat("http://1.1.1.1"))
assert.False(t, checkAddrFormat("http://hostname"))
assert.False(t, checkAddrFormat("http://1.1.1.1:1"))
assert.False(t, checkAddrFormat("http://hostname:1"))
assert.False(t, checkAddrFormat(":1.1.1.1"))
assert.False(t, checkAddrFormat(":hostname"))
assert.False(t, checkAddrFormat(":1.1.1.1:1"))
assert.False(t, checkAddrFormat(":hostname:1"))
assert.True(t, checkAddrFormat("1.1.1.1:1111"))
assert.True(t, checkAddrFormat("hostname:1111"))
assert.True(t, checkAddrFormat("host-name_42:1111"))
assert.False(t, checkAddrFormat("1.1.1.1:-1"))
assert.False(t, checkAddrFormat("1.1.1.1:0"))
assert.True(t, checkAddrFormat("1.1.1.1:65535"))
assert.False(t, checkAddrFormat("1.1.1.1:65536"))
assert.False(t, checkAddrFormat("1.1.1.1: 4000"))
assert.False(t, checkAddrFormat("1.1.1.1:m2"))
assert.True(t, checkAddrFormat("[2001:db8:0:f101::3]:2375"))
assert.False(t, checkAddrFormat("2001:db8:0:f101::3:2375"))
assert.False(t, checkAddrFormat("[2001:db8:0:f101::3]:3:2375"))
}

41
cli/list.go Normal file
View File

@@ -0,0 +1,41 @@
package cli
import (
"fmt"
"log"
"time"
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/discovery"
)
func list(c *cli.Context) {
dflag := getDiscovery(c)
if dflag == "" {
log.Fatalf("discovery required to list a cluster. See '%s list --help'.", c.App.Name)
}
timeout, err := time.ParseDuration(c.String("timeout"))
if err != nil {
log.Fatalf("invalid --timeout: %v", err)
}
if timeout <= time.Duration(0)*time.Second {
log.Fatalf("--timeout should be a positive number")
}
d, err := discovery.New(dflag, timeout, 0, getDiscoveryOpt(c))
if err != nil {
log.Fatal(err)
}
ch, errCh := d.Watch(nil)
select {
case entries := <-ch:
for _, entry := range entries {
fmt.Println(entry)
}
case err := <-errCh:
log.Fatal(err)
case <-time.After(timeout):
log.Fatal("Timed out")
}
}

329
cli/manage.go Normal file
View File

@@ -0,0 +1,329 @@
package cli
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"path"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/discovery"
kvdiscovery "github.com/docker/docker/pkg/discovery/kv"
"github.com/docker/leadership"
"github.com/docker/swarm/api"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/cluster/mesos"
"github.com/docker/swarm/cluster/swarm"
"github.com/docker/swarm/scheduler"
"github.com/docker/swarm/scheduler/filter"
"github.com/docker/swarm/scheduler/strategy"
"github.com/gorilla/mux"
)
const (
leaderElectionPath = "docker/swarm/leader"
defaultRecoverTime = 10 * time.Second
)
type logHandler struct {
}
func (h *logHandler) Handle(e *cluster.Event) error {
id := e.ID
// Trim IDs to 12 chars.
if len(id) > 12 {
id = id[:12]
}
log.WithFields(log.Fields{"node": e.Engine.Name, "id": id, "from": e.From, "status": e.Status}).Debug("Event received")
return nil
}
type statusHandler struct {
cluster cluster.Cluster
candidate *leadership.Candidate
follower *leadership.Follower
}
func (h *statusHandler) Status() [][2]string {
var status [][2]string
if h.candidate != nil && !h.candidate.IsLeader() {
status = [][2]string{
{"Role", "replica"},
{"Primary", h.follower.Leader()},
}
} else {
status = [][2]string{
{"Role", "primary"},
}
}
status = append(status, h.cluster.Info()...)
return status
}
// Load the TLS certificates/keys and, if verify is true, the CA.
func loadTLSConfig(ca, cert, key string, verify bool) (*tls.Config, error) {
c, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?",
cert, key, err)
}
config := &tls.Config{
Certificates: []tls.Certificate{c},
MinVersion: tls.VersionTLS10,
}
if verify {
certPool := x509.NewCertPool()
file, err := ioutil.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("Couldn't read CA certificate: %s", err)
}
certPool.AppendCertsFromPEM(file)
config.RootCAs = certPool
config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = certPool
} else {
// If --tlsverify is not supplied, disable CA validation.
config.InsecureSkipVerify = true
}
return config, nil
}
// Initialize the discovery service.
func createDiscovery(uri string, c *cli.Context) discovery.Backend {
hb, err := time.ParseDuration(c.String("heartbeat"))
if err != nil {
log.Fatalf("invalid --heartbeat: %v", err)
}
if hb < 1*time.Second {
log.Fatal("--heartbeat should be at least one second")
}
// Set up discovery.
discovery, err := discovery.New(uri, hb, 0, getDiscoveryOpt(c))
if err != nil {
log.Fatal(err)
}
return discovery
}
func getDiscoveryOpt(c *cli.Context) map[string]string {
// Process the store options
options := map[string]string{}
for _, option := range c.StringSlice("discovery-opt") {
if !strings.Contains(option, "=") {
log.Fatal("--discovery-opt must contain key=value strings")
}
kvpair := strings.SplitN(option, "=", 2)
options[kvpair[0]] = kvpair[1]
}
if _, ok := options["kv.path"]; !ok {
options["kv.path"] = "docker/swarm/nodes"
}
return options
}
func setupReplication(c *cli.Context, cluster cluster.Cluster, server *api.Server, discovery discovery.Backend, addr string, leaderTTL time.Duration, tlsConfig *tls.Config) {
kvDiscovery, ok := discovery.(*kvdiscovery.Discovery)
if !ok {
log.Fatal("Leader election is only supported with consul, etcd and zookeeper discovery.")
}
client := kvDiscovery.Store()
p := path.Join(kvDiscovery.Prefix(), leaderElectionPath)
candidate := leadership.NewCandidate(client, p, addr, leaderTTL)
follower := leadership.NewFollower(client, p)
primary := api.NewPrimary(cluster, tlsConfig, &statusHandler{cluster, candidate, follower}, c.GlobalBool("debug"), c.Bool("cors"))
replica := api.NewReplica(primary, tlsConfig)
go func() {
for {
run(cluster, candidate, server, primary, replica)
time.Sleep(defaultRecoverTime)
}
}()
go func() {
for {
follow(follower, replica, addr)
time.Sleep(defaultRecoverTime)
}
}()
server.SetHandler(primary)
}
func run(cl cluster.Cluster, candidate *leadership.Candidate, server *api.Server, primary *mux.Router, replica *api.Replica) {
electedCh, errCh := candidate.RunForElection()
var watchdog *cluster.Watchdog
for {
select {
case isElected := <-electedCh:
if isElected {
log.Info("Leader Election: Cluster leadership acquired")
watchdog = cluster.NewWatchdog(cl)
server.SetHandler(primary)
} else {
log.Info("Leader Election: Cluster leadership lost")
cl.UnregisterEventHandler(watchdog)
server.SetHandler(replica)
}
case err := <-errCh:
log.Error(err)
return
}
}
}
func follow(follower *leadership.Follower, replica *api.Replica, addr string) {
leaderCh, errCh := follower.FollowElection()
for {
select {
case leader := <-leaderCh:
if leader == "" {
continue
}
if leader == addr {
replica.SetPrimary("")
} else {
log.Infof("New leader elected: %s", leader)
replica.SetPrimary(leader)
}
case err := <-errCh:
log.Error(err)
return
}
}
}
func manage(c *cli.Context) {
var (
tlsConfig *tls.Config
err error
)
// If either --tls or --tlsverify are specified, load the certificates.
if c.Bool("tls") || c.Bool("tlsverify") {
if !c.IsSet("tlscert") || !c.IsSet("tlskey") {
log.Fatal("--tlscert and --tlskey must be provided when using --tls")
}
if c.Bool("tlsverify") && !c.IsSet("tlscacert") {
log.Fatal("--tlscacert must be provided when using --tlsverify")
}
tlsConfig, err = loadTLSConfig(
c.String("tlscacert"),
c.String("tlscert"),
c.String("tlskey"),
c.Bool("tlsverify"))
if err != nil {
log.Fatal(err)
}
} else {
// Otherwise, if neither --tls nor --tlsverify are specified, abort if
// the other flags are passed as they will be ignored.
if c.IsSet("tlscert") || c.IsSet("tlskey") || c.IsSet("tlscacert") {
log.Fatal("--tlscert, --tlskey and --tlscacert require the use of either --tls or --tlsverify")
}
}
refreshMinInterval := c.Duration("engine-refresh-min-interval")
refreshMaxInterval := c.Duration("engine-refresh-max-interval")
if refreshMinInterval <= time.Duration(0)*time.Second {
log.Fatal("min refresh interval should be a positive number")
}
if refreshMaxInterval < refreshMinInterval {
log.Fatal("max refresh interval cannot be less than min refresh interval")
}
// engine-refresh-retry is deprecated
refreshRetry := c.Int("engine-refresh-retry")
if refreshRetry != 3 {
log.Fatal("--engine-refresh-retry is deprecated. Use --engine-failure-retry")
}
failureRetry := c.Int("engine-failure-retry")
if failureRetry <= 0 {
log.Fatal("invalid failure retry count")
}
engineOpts := &cluster.EngineOpts{
RefreshMinInterval: refreshMinInterval,
RefreshMaxInterval: refreshMaxInterval,
FailureRetry: failureRetry,
}
uri := getDiscovery(c)
if uri == "" {
log.Fatalf("discovery required to manage a cluster. See '%s manage --help'.", c.App.Name)
}
discovery := createDiscovery(uri, c)
s, err := strategy.New(c.String("strategy"))
if err != nil {
log.Fatal(err)
}
// see https://github.com/codegangsta/cli/issues/160
names := c.StringSlice("filter")
if c.IsSet("filter") || c.IsSet("f") {
names = names[DefaultFilterNumber:]
}
fs, err := filter.New(names)
if err != nil {
log.Fatal(err)
}
sched := scheduler.New(s, fs)
var cl cluster.Cluster
switch c.String("cluster-driver") {
case "mesos-experimental":
log.Warn("WARNING: the mesos driver is currently experimental, use at your own risks")
cl, err = mesos.NewCluster(sched, tlsConfig, uri, c.StringSlice("cluster-opt"), engineOpts)
case "swarm":
cl, err = swarm.NewCluster(sched, tlsConfig, discovery, c.StringSlice("cluster-opt"), engineOpts)
default:
log.Fatalf("unsupported cluster %q", c.String("cluster-driver"))
}
if err != nil {
log.Fatal(err)
}
// see https://github.com/codegangsta/cli/issues/160
hosts := c.StringSlice("host")
if c.IsSet("host") || c.IsSet("H") {
hosts = hosts[1:]
}
server := api.NewServer(hosts, tlsConfig)
if c.Bool("replication") {
addr := c.String("advertise")
if addr == "" {
log.Fatal("--advertise address must be provided when using --leader-election")
}
if !checkAddrFormat(addr) {
log.Fatal("--advertise should be of the form ip:port or hostname:port")
}
leaderTTL, err := time.ParseDuration(c.String("replication-ttl"))
if err != nil {
log.Fatalf("invalid --replication-ttl: %v", err)
}
if leaderTTL <= time.Duration(0)*time.Second {
log.Fatalf("--replication-ttl should be a positive number")
}
setupReplication(c, cl, server, discovery, addr, leaderTTL, tlsConfig)
} else {
server.SetHandler(api.NewPrimary(cl, tlsConfig, &statusHandler{cl, nil, nil}, c.GlobalBool("debug"), c.Bool("cors")))
cluster.NewWatchdog(cl)
}
log.Fatal(server.ListenAndServe())
}

102
cluster/cluster.go Normal file
View File

@@ -0,0 +1,102 @@
package cluster
import (
"io"
"github.com/docker/engine-api/types"
"github.com/samalba/dockerclient"
)
// Cluster is exported
type Cluster interface {
// Create a container
CreateContainer(config *ContainerConfig, name string, authConfig *types.AuthConfig) (*Container, error)
// Remove a container
RemoveContainer(container *Container, force, volumes bool) error
// Return all images
Images() Images
// Return one image matching `IDOrName`
Image(IDOrName string) *Image
// Remove images from the cluster
RemoveImages(name string, force bool) ([]types.ImageDelete, error)
// Return all containers
Containers() Containers
// Start a container
StartContainer(container *Container, hostConfig *dockerclient.HostConfig) error
// Return container the matching `IDOrName`
// TODO: remove this method from the interface as we can use
// cluster.Containers().Get(IDOrName)
Container(IDOrName string) *Container
// Return all networks
Networks() Networks
// Create a network
CreateNetwork(name string, request *types.NetworkCreate) (*types.NetworkCreateResponse, error)
// Remove a network from the cluster
RemoveNetwork(network *Network) error
// Create a volume
CreateVolume(request *types.VolumeCreateRequest) (*types.Volume, error)
// Return all volumes
Volumes() Volumes
// Remove volumes from the cluster
RemoveVolumes(name string) (bool, error)
// Pull images
// `callback` can be called multiple time
// `where` is where it is being pulled
// `status` is the current status, like "", "in progress" or "downloaded"
Pull(name string, authConfig *types.AuthConfig, callback func(where, status string, err error))
// Import image
// `callback` can be called multiple time
// `where` is where it is being imported
// `status` is the current status, like "", "in progress" or "imported"
Import(source string, ref string, tag string, imageReader io.Reader, callback func(where, status string, err error))
// Load images
// `callback` can be called multiple time
// `what` is what is being loaded
// `status` is the current status, like "", "in progress" or "loaded"
Load(imageReader io.Reader, callback func(what, status string, err error))
// Return some info about the cluster, like nb of containers / images
// It is pretty open, so the implementation decides what to return.
Info() [][2]string
// Return the total memory of the cluster
TotalMemory() int64
// Return the number of CPUs in the cluster
TotalCpus() int64
// Register an event handler for cluster-wide events.
RegisterEventHandler(h EventHandler) error
// Unregister an event handler.
UnregisterEventHandler(h EventHandler)
// FIXME: remove this method
// Return a random engine
RANDOMENGINE() (*Engine, error)
// Rename a container
RenameContainer(container *Container, newName string) error
// Build an image
BuildImage(io.Reader, *types.ImageBuildOptions, io.Writer) error
// Tag an image
TagImage(IDOrName string, ref string, force bool) error
}

250
cluster/config.go Normal file
View File

@@ -0,0 +1,250 @@
package cluster
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/network"
)
// SwarmLabelNamespace defines the key prefix in all custom labels
const SwarmLabelNamespace = "com.docker.swarm"
// ContainerConfig is exported
// TODO store affinities and constraints in their own fields
type ContainerConfig struct {
container.Config
HostConfig container.HostConfig
NetworkingConfig network.NetworkingConfig
}
// OldContainerConfig contains additional fields for backward compatibility
// This should be removed after we stop supporting API versions <= 1.8
type OldContainerConfig struct {
ContainerConfig
Memory int64
MemorySwap int64
CPUShares int64 `json:"CpuShares"`
CPUSet string `json:"Cpuset"`
}
func parseEnv(e string) (bool, string, string) {
parts := strings.SplitN(e, ":", 2)
if len(parts) == 2 {
return true, parts[0], parts[1]
}
return false, "", ""
}
// ConsolidateResourceFields is a temporary fix to handle forward/backward compatibility between Docker <1.6 and >=1.7
func ConsolidateResourceFields(c *OldContainerConfig) {
if c.Memory != c.HostConfig.Memory {
if c.Memory != 0 {
c.HostConfig.Memory = c.Memory
}
}
if c.MemorySwap != c.HostConfig.MemorySwap {
if c.MemorySwap != 0 {
c.HostConfig.MemorySwap = c.MemorySwap
}
}
if c.CPUShares != c.HostConfig.CPUShares {
if c.CPUShares != 0 {
c.HostConfig.CPUShares = c.CPUShares
}
}
if c.CPUSet != c.HostConfig.CpusetCpus {
if c.CPUSet != "" {
c.HostConfig.CpusetCpus = c.CPUSet
}
}
}
// BuildContainerConfig creates a cluster.ContainerConfig from a Config, HostConfig, and NetworkingConfig
func BuildContainerConfig(c container.Config, h container.HostConfig, n network.NetworkingConfig) *ContainerConfig {
var (
affinities []string
constraints []string
reschedulePolicies []string
env []string
)
// only for tests
if c.Labels == nil {
c.Labels = make(map[string]string)
}
// parse affinities from labels (ex. docker run --label 'com.docker.swarm.affinities=["container==redis","image==nginx"]')
if labels, ok := c.Labels[SwarmLabelNamespace+".affinities"]; ok {
json.Unmarshal([]byte(labels), &affinities)
}
// parse constraints from labels (ex. docker run --label 'com.docker.swarm.constraints=["region==us-east","storage==ssd"]')
if labels, ok := c.Labels[SwarmLabelNamespace+".constraints"]; ok {
json.Unmarshal([]byte(labels), &constraints)
}
// parse reschedule policy from labels (ex. docker run --label 'com.docker.swarm.reschedule-policies=["on-node-failure"]')
if labels, ok := c.Labels[SwarmLabelNamespace+".reschedule-policies"]; ok {
json.Unmarshal([]byte(labels), &reschedulePolicies)
}
// parse affinities/constraints/reschedule policies from env (ex. docker run -e affinity:container==redis -e affinity:image==nginx -e constraint:region==us-east -e constraint:storage==ssd -e reschedule:off)
for _, e := range c.Env {
if ok, key, value := parseEnv(e); ok && key == "affinity" {
affinities = append(affinities, value)
} else if ok && key == "constraint" {
constraints = append(constraints, value)
} else if ok && key == "reschedule" {
reschedulePolicies = append(reschedulePolicies, value)
} else {
env = append(env, e)
}
}
// remove affinities/constraints/reschedule policies from env
c.Env = env
// store affinities in labels
if len(affinities) > 0 {
if labels, err := json.Marshal(affinities); err == nil {
c.Labels[SwarmLabelNamespace+".affinities"] = string(labels)
}
}
// store constraints in labels
if len(constraints) > 0 {
if labels, err := json.Marshal(constraints); err == nil {
c.Labels[SwarmLabelNamespace+".constraints"] = string(labels)
}
}
// store reschedule policies in labels
if len(reschedulePolicies) > 0 {
if labels, err := json.Marshal(reschedulePolicies); err == nil {
c.Labels[SwarmLabelNamespace+".reschedule-policies"] = string(labels)
}
}
return &ContainerConfig{c, h, n}
}
func (c *ContainerConfig) extractExprs(key string) []string {
var exprs []string
if labels, ok := c.Labels[SwarmLabelNamespace+"."+key]; ok {
json.Unmarshal([]byte(labels), &exprs)
}
return exprs
}
// SwarmID extracts the Swarm ID from the Config.
// May return an empty string if not set.
func (c *ContainerConfig) SwarmID() string {
return c.Labels[SwarmLabelNamespace+".id"]
}
// SetSwarmID sets or overrides the Swarm ID in the Config.
func (c *ContainerConfig) SetSwarmID(id string) {
c.Labels[SwarmLabelNamespace+".id"] = id
}
// Affinities returns all the affinities from the ContainerConfig
func (c *ContainerConfig) Affinities() []string {
return c.extractExprs("affinities")
}
// Constraints returns all the constraints from the ContainerConfig
func (c *ContainerConfig) Constraints() []string {
return c.extractExprs("constraints")
}
// AddAffinity to config
func (c *ContainerConfig) AddAffinity(affinity string) error {
affinities := c.extractExprs("affinities")
affinities = append(affinities, affinity)
labels, err := json.Marshal(affinities)
if err != nil {
return err
}
c.Labels[SwarmLabelNamespace+".affinities"] = string(labels)
return nil
}
// RemoveAffinity from config
func (c *ContainerConfig) RemoveAffinity(affinity string) error {
affinities := []string{}
for _, a := range c.extractExprs("affinities") {
if a != affinity {
affinities = append(affinities, a)
}
}
labels, err := json.Marshal(affinities)
if err != nil {
return err
}
c.Labels[SwarmLabelNamespace+".affinities"] = string(labels)
return nil
}
// AddConstraint to config
func (c *ContainerConfig) AddConstraint(constraint string) error {
constraints := c.extractExprs("constraints")
constraints = append(constraints, constraint)
labels, err := json.Marshal(constraints)
if err != nil {
return err
}
c.Labels[SwarmLabelNamespace+".constraints"] = string(labels)
return nil
}
// HaveNodeConstraint in config
func (c *ContainerConfig) HaveNodeConstraint() bool {
constraints := c.extractExprs("constraints")
for _, constraint := range constraints {
if strings.HasPrefix(constraint, "node==") && !strings.HasPrefix(constraint, "node==~") {
return true
}
}
return false
}
// HasReschedulePolicy returns true if the specified policy is part of the config
func (c *ContainerConfig) HasReschedulePolicy(p string) bool {
for _, reschedulePolicy := range c.extractExprs("reschedule-policies") {
if reschedulePolicy == p {
return true
}
}
return false
}
// Validate returns an error if the config isn't valid
func (c *ContainerConfig) Validate() error {
//TODO: add validation for affinities and constraints
reschedulePolicies := c.extractExprs("reschedule-policies")
if len(reschedulePolicies) > 1 {
return errors.New("too many reschedule policies")
} else if len(reschedulePolicies) == 1 {
valid := false
for _, validReschedulePolicy := range []string{"off", "on-node-failure"} {
if reschedulePolicies[0] == validReschedulePolicy {
valid = true
}
}
if !valid {
return fmt.Errorf("invalid reschedule policy: %s", reschedulePolicies[0])
}
}
return nil
}

106
cluster/config_test.go Normal file
View File

@@ -0,0 +1,106 @@
package cluster
import (
"testing"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/network"
"github.com/stretchr/testify/assert"
)
func TestBuildContainerConfig(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Env)
assert.Empty(t, config.Labels)
config = BuildContainerConfig(container.Config{Env: []string{"test=true"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Env, 1)
assert.Empty(t, config.Labels)
config = BuildContainerConfig(container.Config{Env: []string{"constraint:test==true"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Env)
assert.Len(t, config.Labels, 1)
config = BuildContainerConfig(container.Config{Env: []string{"affinity:container==test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Env)
assert.Len(t, config.Labels, 1)
config = BuildContainerConfig(container.Config{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Env, 1)
assert.Len(t, config.Labels, 2)
}
func TestSwarmID(t *testing.T) {
// Getter / Setter
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.SwarmID())
config.SetSwarmID("foo")
assert.Equal(t, config.SwarmID(), "foo")
assert.Equal(t, config.Labels[SwarmLabelNamespace+".id"], "foo")
// Retrieve an existing ID.
config = BuildContainerConfig(container.Config{Labels: map[string]string{SwarmLabelNamespace + ".id": "test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Equal(t, config.SwarmID(), "test")
}
func TestConstraints(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Constraints())
config = BuildContainerConfig(container.Config{Env: []string{"constraint:test==true"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Constraints(), 1)
config = BuildContainerConfig(container.Config{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Constraints(), 1)
}
func TestAffinities(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Affinities())
config = BuildContainerConfig(container.Config{Env: []string{"affinity:container==test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Affinities(), 1)
config = BuildContainerConfig(container.Config{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.Len(t, config.Affinities(), 1)
assert.Equal(t, len(config.Affinities()), 1)
}
func TestConsolidateResourceFields(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{Resources: container.Resources{Memory: 4242, MemorySwap: 4343, CPUShares: 4444, CpusetCpus: "1-2"}}, network.NetworkingConfig{})
assert.Equal(t, config.HostConfig.Memory, int64(4242))
assert.Equal(t, config.HostConfig.MemorySwap, int64(4343))
assert.Equal(t, config.HostConfig.CPUShares, int64(4444))
assert.Equal(t, config.HostConfig.CpusetCpus, "1-2")
}
func TestAddAffinity(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Affinities())
config.AddAffinity("image==~testimage")
assert.Len(t, config.Affinities(), 1)
}
func TestRemoveAffinity(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.Empty(t, config.Affinities())
config.AddAffinity("image==~testimage1")
config.AddAffinity("image==~testimage2")
assert.Len(t, config.Affinities(), 2)
config.RemoveAffinity("image==~testimage1")
assert.Len(t, config.Affinities(), 1)
assert.Equal(t, config.Affinities()[0], "image==~testimage2")
}
func TestHaveNodeConstraint(t *testing.T) {
config := BuildContainerConfig(container.Config{}, container.HostConfig{}, network.NetworkingConfig{})
assert.False(t, config.HaveNodeConstraint())
config = BuildContainerConfig(container.Config{Env: []string{"constraint:node==node1"}}, container.HostConfig{}, network.NetworkingConfig{})
assert.True(t, config.HaveNodeConstraint())
}

157
cluster/container.go Normal file
View File

@@ -0,0 +1,157 @@
package cluster
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/engine-api/types"
"github.com/docker/go-units"
)
// Container is exported
type Container struct {
types.Container
Config *ContainerConfig
Info types.ContainerJSON
Engine *Engine
}
// StateString returns a single string to describe state
func StateString(state *types.ContainerState) string {
startedAt, _ := time.Parse(time.RFC3339Nano, state.StartedAt)
if state.Running {
if state.Paused {
return "paused"
}
if state.Restarting {
return "restarting"
}
return "running"
}
if state.Dead {
return "dead"
}
if startedAt.IsZero() {
return "created"
}
return "exited"
}
// FullStateString returns human-readable description of the state
func FullStateString(state *types.ContainerState) string {
startedAt, _ := time.Parse(time.RFC3339Nano, state.StartedAt)
finishedAt, _ := time.Parse(time.RFC3339Nano, state.FinishedAt)
if state.Running {
if state.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(startedAt)))
}
if state.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago", state.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt)))
}
// Container is Up. Add Health check info when healthcheck is defined.
healthText := ""
if h := state.Health; h != nil {
switch h.Status {
case types.Starting:
healthText = "health: starting"
default: // Healthy and Unhealthy are clear on their own
healthText = h.Status
}
}
if len(healthText) > 0 {
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(startedAt)), healthText)
}
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(startedAt)))
}
if state.Dead {
return "Dead"
}
if startedAt.IsZero() {
return "Created"
}
if finishedAt.IsZero() {
return ""
}
return fmt.Sprintf("Exited (%d) %s ago", state.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt)))
}
// Refresh container
func (c *Container) Refresh() (*Container, error) {
return c.Engine.refreshContainer(c.ID, true)
}
// Containers represents a list of containers
type Containers []*Container
// Get returns a container using its ID or Name
func (containers Containers) Get(IDOrName string) *Container {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
// Match exact or short Container ID.
for _, container := range containers {
if container.ID == IDOrName || stringid.TruncateID(container.ID) == IDOrName {
return container
}
}
// Match exact Swarm ID.
for _, container := range containers {
if swarmID := container.Config.SwarmID(); swarmID == IDOrName || stringid.TruncateID(swarmID) == IDOrName {
return container
}
}
candidates := []*Container{}
// Match name, /name or engine/name.
for _, container := range containers {
found := false
for _, name := range container.Names {
if name == IDOrName || name == "/"+IDOrName || container.Engine.ID+name == IDOrName || container.Engine.Name+name == IDOrName {
found = true
}
}
if found {
candidates = append(candidates, container)
}
}
if size := len(candidates); size == 1 {
return candidates[0]
} else if size > 1 {
return nil
}
// Match Container ID prefix.
for _, container := range containers {
if strings.HasPrefix(container.ID, IDOrName) {
candidates = append(candidates, container)
}
}
// Match Swarm ID prefix.
for _, container := range containers {
if strings.HasPrefix(container.Config.SwarmID(), IDOrName) {
candidates = append(candidates, container)
}
}
if len(candidates) == 1 {
return candidates[0]
}
return nil
}

60
cluster/container_test.go Normal file
View File

@@ -0,0 +1,60 @@
package cluster
import (
"testing"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/stretchr/testify/assert"
)
func TestContainersGet(t *testing.T) {
containers := Containers([]*Container{{
Container: types.Container{
ID: "container1-id",
Names: []string{"/container1-name1", "/container1-name2"},
},
Engine: &Engine{ID: "test-engine"},
Config: BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.id": "swarm1-id",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}, {
Container: types.Container{
ID: "container2-id",
Names: []string{"/con"},
},
Engine: &Engine{ID: "test-engine"},
Config: BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.id": "swarm2-id",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}})
// Invalid lookup
assert.Nil(t, containers.Get("invalid-id"))
assert.Nil(t, containers.Get(""))
// Container ID lookup.
assert.NotNil(t, containers.Get("container1-id"))
// Container ID prefix lookup.
assert.NotNil(t, containers.Get("container1-"))
assert.Nil(t, containers.Get("container"))
// Container name lookup.
assert.NotNil(t, containers.Get("container1-name1"))
assert.NotNil(t, containers.Get("container1-name2"))
// Container engine/name matching.
assert.NotNil(t, containers.Get("test-engine/container1-name1"))
assert.NotNil(t, containers.Get("test-engine/container1-name2"))
// Swarm ID lookup.
assert.NotNil(t, containers.Get("swarm1-id"))
// Swarm ID prefix lookup.
assert.NotNil(t, containers.Get("swarm1-"))
assert.Nil(t, containers.Get("swarm"))
// Get name before ID prefix
cc := containers.Get("con")
assert.NotNil(t, cc)
assert.Equal(t, cc.ID, "container2-id")
}

1402
cluster/engine.go Normal file

File diff suppressed because it is too large Load Diff

25
cluster/engine_sorter.go Normal file
View File

@@ -0,0 +1,25 @@
package cluster
import (
"github.com/skarademir/naturalsort"
)
// EngineSorter implements the Sort interface to sort Cluster.Engine.
// It is not guaranteed to be a stable sort.
type EngineSorter []*Engine
// Len returns the number of engines to be sorted.
func (s EngineSorter) Len() int {
return len(s)
}
// Swap exchanges the engine elements with indices i and j.
func (s EngineSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the engine with index i should sort before the engine with index j.
// Engines are sorted chronologically by name.
func (s EngineSorter) Less(i, j int) bool {
return naturalsort.NaturalSort([]string{s[i].Name, s[j].Name}).Less(0, 1)
}

View File

@@ -0,0 +1,32 @@
package cluster
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEngineSorter(t *testing.T) {
engines := []*Engine{{Name: "name1"}, {Name: "name3"}, {Name: "name2"}}
sort.Sort(EngineSorter(engines))
assert.Equal(t, engines[0].Name, "name1")
assert.Equal(t, engines[1].Name, "name2")
assert.Equal(t, engines[2].Name, "name3")
}
func TestNaturalSort(t *testing.T) {
engines := []*Engine{{Name: "machine10"},
{Name: "machine9"},
{Name: "machine1"},
{Name: "machine11"}}
sort.Sort(EngineSorter(engines))
assert.Equal(t, engines[0].Name, "machine1")
assert.Equal(t, engines[1].Name, "machine9")
assert.Equal(t, engines[2].Name, "machine10")
assert.Equal(t, engines[3].Name, "machine11")
}

847
cluster/engine_test.go Normal file
View File

@@ -0,0 +1,847 @@
package cluster
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"testing"
"time"
engineapi "github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/events"
"github.com/docker/engine-api/types/filters"
networktypes "github.com/docker/engine-api/types/network"
engineapimock "github.com/docker/swarm/api/mockclient"
engineapinop "github.com/docker/swarm/api/nopclient"
"github.com/samalba/dockerclient"
"github.com/samalba/dockerclient/mockclient"
"github.com/samalba/dockerclient/nopclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type infiniteRead struct{}
func (infiniteRead) Read(p []byte) (n int, err error) {
p = append(p, 1)
return 1, nil
}
var (
mockInfo = types.Info{
ID: "id",
Name: "name",
NCPU: 10,
MemTotal: 20,
Driver: "driver-test",
ExecutionDriver: "execution-driver-test",
KernelVersion: "1.2.3",
OperatingSystem: "golang",
Labels: []string{"foo=bar"},
}
mockVersion = types.Version{
Version: "1.8.2",
}
engOpts = &EngineOpts{
RefreshMinInterval: time.Duration(30) * time.Second,
RefreshMaxInterval: time.Duration(60) * time.Second,
FailureRetry: 3,
}
)
type nopCloser struct {
io.Reader
}
// Close
func (nopCloser) Close() error {
return nil
}
func TestSetEngineState(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
assert.True(t, engine.state == statePending)
engine.setState(stateUnhealthy)
assert.True(t, engine.state == stateUnhealthy)
engine.setState(stateHealthy)
assert.True(t, engine.state == stateHealthy)
}
func TestErrMsg(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
assert.True(t, len(engine.ErrMsg()) == 0)
message := "cannot connect"
engine.setErrMsg(message)
assert.True(t, engine.ErrMsg() == message)
}
func TestCheckConnectionErr(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateHealthy)
assert.True(t, engine.failureCount == 0)
err := dockerclient.ErrConnectionRefused
engine.CheckConnectionErr(err)
assert.True(t, len(engine.ErrMsg()) > 0)
assert.True(t, engine.failureCount == 1)
err = engineapi.ErrConnectionFailed
engine.CheckConnectionErr(err)
assert.True(t, engine.failureCount == 2)
err = nil
engine.CheckConnectionErr(err)
assert.True(t, engine.failureCount == 0)
assert.True(t, len(engine.ErrMsg()) == 0)
// Do not accept random error
err = fmt.Errorf("random error")
engine.CheckConnectionErr(err)
assert.True(t, engine.failureCount == 0)
assert.True(t, len(engine.ErrMsg()) == 0)
}
func TestEngineFailureCount(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateHealthy)
for i := 0; i < engine.opts.FailureRetry; i++ {
assert.True(t, engine.IsHealthy())
engine.incFailureCount()
}
assert.False(t, engine.IsHealthy())
assert.True(t, engine.failureCount == engine.opts.FailureRetry)
engine.resetFailureCount()
assert.True(t, engine.failureCount == 0)
}
func TestHealthIndicator(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
assert.True(t, engine.state == statePending)
assert.True(t, engine.HealthIndicator() == 0)
engine.setState(stateUnhealthy)
assert.True(t, engine.HealthIndicator() == 0)
engine.setState(stateHealthy)
assert.True(t, engine.HealthIndicator() == 100)
engine.incFailureCount()
assert.True(t, engine.HealthIndicator() == (int64)(100-100/engine.opts.FailureRetry))
}
func TestEngineConnectionFailure(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
assert.False(t, engine.isConnected())
// Always fail.
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(types.Info{}, errors.New("fail"))
// Connect() should fail
assert.Error(t, engine.ConnectWithClient(client, apiClient))
// isConnected() should return false
nop := nopclient.NewNopClient()
nopAPIClient := engineapinop.NewNopClient()
assert.Error(t, engine.ConnectWithClient(nop, nopAPIClient))
assert.False(t, engine.isConnected())
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestOutdatedEngine(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(types.Info{}, nil)
assert.Error(t, engine.ConnectWithClient(client, apiClient))
nop := nopclient.NewNopClient()
nopAPIClient := engineapinop.NewNopClient()
assert.Error(t, engine.ConnectWithClient(nop, nopAPIClient))
assert.False(t, engine.isConnected())
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestEngineCpusMemory(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateUnhealthy)
assert.False(t, engine.isConnected())
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.True(t, engine.isConnected())
assert.True(t, engine.IsHealthy())
assert.Equal(t, engine.UsedCpus(), int64(0))
assert.Equal(t, engine.UsedMemory(), int64(0))
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestEngineSpecs(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateUnhealthy)
assert.False(t, engine.isConnected())
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.True(t, engine.isConnected())
assert.True(t, engine.IsHealthy())
mockInfo2 := mockInfo
mockInfo2.Labels = []string{"foo=bar", "executiondriver=newdriver", "node=node1"}
assert.Equal(t, engine.Cpus, int64(mockInfo2.NCPU))
assert.Equal(t, engine.Memory, mockInfo2.MemTotal)
assert.Equal(t, engine.Labels["storagedriver"], mockInfo2.Driver)
assert.Equal(t, engine.Labels["executiondriver"], mockInfo2.ExecutionDriver)
assert.Equal(t, engine.Labels["kernelversion"], mockInfo2.KernelVersion)
assert.Equal(t, engine.Labels["operatingsystem"], mockInfo2.OperatingSystem)
assert.Equal(t, engine.Labels["foo"], "bar")
assert.NotEqual(t, engine.Labels["node"], "node1")
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestEngineState(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateUnhealthy)
assert.False(t, engine.isConnected())
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
// The client will return one container at first, then a second one will appear.
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil).Once()
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
},
).Return(
[]types.Container{
{
ID: "one",
},
},
nil,
).Once()
apiClient.On("ContainerInspect", mock.Anything, "one").Return(
types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 100,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
filterArgs := filters.NewArgs()
filterArgs.Add("id", "two")
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
Filter: filterArgs,
},
).Return(
[]types.Container{{ID: "two"}},
nil,
).Once()
apiClient.On("ContainerInspect", mock.Anything, "two").Return(
types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 100,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.True(t, engine.isConnected())
// The engine should only have a single container at this point.
containers := engine.Containers()
assert.Len(t, containers, 1)
if containers[0].ID != "one" {
t.Fatalf("Missing container: one")
}
// Fake an event which will trigger a refresh. The second container will appear.
engine.handler(events.Message{ID: "two", Status: "created"})
containers = engine.Containers()
assert.Len(t, containers, 2)
if containers[0].ID != "one" && containers[1].ID != "one" {
t.Fatalf("Missing container: one")
}
if containers[0].ID != "two" && containers[1].ID != "two" {
t.Fatalf("Missing container: two")
}
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestCreateContainer(t *testing.T) {
var (
config = &ContainerConfig{containertypes.Config{
Image: "busybox",
Cmd: []string{"date"},
Tty: false,
}, containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 1,
},
}, networktypes.NetworkingConfig{}}
state = types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
}
engine = NewEngine("test", 0, engOpts)
client = mockclient.NewMockClient()
apiClient = engineapimock.NewMockClient()
readCloser = nopCloser{bytes.NewBufferString("")}
)
engine.setState(stateUnhealthy)
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once()
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil).Once()
// filterArgs1 := filters.NewArgs()
// filterArgs1.Add("id", id)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil).Once()
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.True(t, engine.isConnected())
mockConfig := *config
mockConfig.HostConfig.CPUShares = int64(math.Ceil(float64(config.HostConfig.CPUShares*1024) / float64(mockInfo.NCPU)))
// Everything is ok
name := "test1"
id := "id1"
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil).Once()
var auth *types.AuthConfig
apiClient.On(
"ContainerCreate",
mock.Anything,
&mockConfig.Config,
&mockConfig.HostConfig,
&mockConfig.NetworkingConfig,
name,
).Return(
types.ContainerCreateResponse{ID: id},
nil,
).Once()
filterArgs := filters.NewArgs()
filterArgs.Add("id", id)
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
Filter: filterArgs,
},
).Return(
[]types.Container{{ID: id}},
nil,
).Once()
apiClient.On(
"ContainerInspect",
mock.Anything,
id,
).Return(
types.ContainerJSON{
Config: &config.Config,
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &config.HostConfig,
State: &state,
},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
container, err := engine.CreateContainer(config, name, false, auth)
assert.Nil(t, err)
assert.Equal(t, container.ID, id)
assert.Len(t, engine.Containers(), 1)
// Image not found, pullImage == false
name = "test2"
mockConfig.HostConfig.CPUShares = int64(math.Ceil(float64(config.HostConfig.CPUShares*1024) / float64(mockInfo.NCPU)))
// FIXMEENGINEAPI : below should return an engine-api error, or something custom
apiClient.On(
"ContainerCreate",
mock.Anything,
&mockConfig.Config,
&mockConfig.HostConfig,
&mockConfig.NetworkingConfig,
name,
).Return(
types.ContainerCreateResponse{},
dockerclient.ErrImageNotFound,
).Once()
container, err = engine.CreateContainer(config, name, false, auth)
assert.Equal(t, err, dockerclient.ErrImageNotFound)
assert.Nil(t, container)
// Image not found, pullImage == true, and the image can be pulled successfully
name = "test3"
id = "id3"
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil).Once()
mockConfig.HostConfig.CPUShares = int64(math.Ceil(float64(config.HostConfig.CPUShares*1024) / float64(mockInfo.NCPU)))
apiClient.On("ImagePull", mock.Anything, config.Image, mock.AnythingOfType("types.ImagePullOptions")).Return(readCloser, nil).Once()
// TODO(nishanttotla): below should return an engine-api error, or something custom, so that we can get rid of dockerclient
apiClient.On(
"ContainerCreate",
mock.Anything,
&mockConfig.Config,
&mockConfig.HostConfig,
&mockConfig.NetworkingConfig,
name,
).Return(
types.ContainerCreateResponse{},
dockerclient.ErrImageNotFound,
).Once()
// FIXMEENGINEAPI : below should return an engine-api error, or something custom
apiClient.On(
"ContainerCreate",
mock.Anything,
&mockConfig.Config,
&mockConfig.HostConfig,
&mockConfig.NetworkingConfig,
name,
).Return(
types.ContainerCreateResponse{ID: id},
nil,
).Once()
filterArgs = filters.NewArgs()
filterArgs.Add("id", id)
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
Filter: filterArgs,
},
).Return(
[]types.Container{{ID: id}},
nil,
).Once()
apiClient.On("ContainerInspect", mock.Anything, id).Return(
types.ContainerJSON{
Config: &config.Config,
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &config.HostConfig,
State: &state,
},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
container, err = engine.CreateContainer(config, name, true, auth)
assert.Nil(t, err)
assert.Equal(t, container.ID, id)
assert.Len(t, engine.Containers(), 2)
}
func TestImages(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
engine.setState(stateHealthy)
engine.images = []*Image{
{types.Image{ID: "a"}, engine},
{types.Image{ID: "b"}, engine},
{types.Image{ID: "c"}, engine},
}
result := engine.Images()
assert.Equal(t, len(result), 3)
}
func TestTotalMemory(t *testing.T) {
engine := NewEngine("test", 0.05, engOpts)
engine.Memory = 1024
assert.Equal(t, engine.TotalMemory(), int64(1024+1024*5/100))
engine = NewEngine("test", 0, engOpts)
engine.Memory = 1024
assert.Equal(t, engine.TotalMemory(), int64(1024))
}
func TestTotalCpus(t *testing.T) {
engine := NewEngine("test", 0.05, engOpts)
engine.Cpus = 2
assert.Equal(t, engine.TotalCpus(), int64(2+2*5/100))
engine = NewEngine("test", 0, engOpts)
engine.Cpus = 2
assert.Equal(t, engine.TotalCpus(), int64(2))
}
func TestUsedCpus(t *testing.T) {
var (
containerNcpu = []int64{1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47}
hostNcpu = []int64{1, 2, 4, 8, 10, 12, 16, 20, 32, 36, 40, 48}
)
engine := NewEngine("test", 0, engOpts)
engine.setState(stateHealthy)
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
for _, hn := range hostNcpu {
for _, cn := range containerNcpu {
if cn <= hn {
mockInfo.NCPU = int(hn)
cpuShares := int64(math.Ceil(float64(cn*1024) / float64(mockInfo.NCPU)))
apiClient.On("Info", mock.Anything).Return(mockInfo, nil).Once()
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil).Once()
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
},
).Return(
[]types.Container{{ID: "test"}},
nil,
).Once()
apiClient.On("ContainerInspect", mock.Anything, "test").Return(
types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: cpuShares,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
engine.ConnectWithClient(client, apiClient)
assert.Equal(t, engine.Cpus, int64(mockInfo.NCPU))
assert.Equal(t, engine.UsedCpus(), cn)
}
}
}
}
func TestContainerRemovedDuringRefresh(t *testing.T) {
var (
container1 = types.Container{ID: "c1"}
container2 = types.Container{ID: "c2"}
info1 types.ContainerJSON
info2 = types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 100,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
}
)
engine := NewEngine("test", 0, engOpts)
engine.setState(stateUnhealthy)
assert.False(t, engine.isConnected())
// A container is removed before it can be inspected.
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
},
).Return(
[]types.Container{container1, container2},
nil,
)
apiClient.On("ContainerInspect", mock.Anything, "c1").Return(info1, errors.New("Not found"))
apiClient.On("ContainerInspect", mock.Anything, "c2").Return(info2, nil)
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.Nil(t, engine.RefreshContainers(true))
// List of containers is still valid
containers := engine.Containers()
assert.Len(t, containers, 1)
assert.Equal(t, containers[0].ID, "c2")
client.Mock.AssertExpectations(t)
apiClient.Mock.AssertExpectations(t)
}
func TestDisconnect(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything,
mock.AnythingOfType("Args"),
).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{infiniteRead{}}, nil)
// The client will return one container at first, then a second one will appear.
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
},
).Return([]types.Container{{ID: "one"}}, nil).Once()
apiClient.On(
"ContainerInspect",
mock.Anything,
"one",
).Return(
types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 100,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
filterArgs := filters.NewArgs()
filterArgs.Add("id", "two")
apiClient.On(
"ContainerList",
mock.Anything,
types.ContainerListOptions{
All: true,
Size: false,
Filter: filterArgs,
},
).Return(
[]types.Container{{ID: "two"}},
nil,
).Once()
apiClient.On("ContainerInspect", mock.Anything, "two").Return(
types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 100,
},
},
State: &types.ContainerState{
StartedAt: "2016-06-06T01:41:38.090313266Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
},
Config: &containertypes.Config{},
NetworkSettings: &types.NetworkSettings{
Networks: nil,
},
},
nil,
).Once()
assert.NoError(t, engine.ConnectWithClient(client, apiClient))
assert.True(t, engine.isConnected())
defer func() {
if r := recover(); r != nil {
t.Errorf("TestDisconnect causes panic")
}
}()
engine.Disconnect()
assert.False(t, engine.isConnected())
assert.True(t, engine.state == stateDisconnected)
// Double disconnect shouldn't cause panic
engine.Disconnect()
}
func TestRemoveImage(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
imageName := "test-image"
dIs := []types.ImageDelete{{Deleted: imageName}}
apiClient := engineapimock.NewMockClient()
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("ImageRemove", mock.Anything, mock.Anything,
mock.AnythingOfType("ImageRemoveOptions")).Return(dIs, nil)
engine.apiClient = apiClient
deletedImages, err := engine.RemoveImage("test-image", true)
if err != nil {
t.Errorf("encountered an unexpected error")
}
if deletedImages[0].Deleted != imageName {
t.Errorf("didn't get the image we removed")
}
apiClient.Mock.AssertExpectations(t)
}

66
cluster/event.go Normal file
View File

@@ -0,0 +1,66 @@
package cluster
import (
"errors"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types/events"
)
// Event is exported
type Event struct {
events.Message
Engine *Engine `json:"-"`
}
// EventHandler is exported
type EventHandler interface {
Handle(*Event) error
}
// EventHandlers is a map of EventHandler
type EventHandlers struct {
sync.RWMutex
eventHandlers map[EventHandler]struct{}
}
// NewEventHandlers returns an EventHandlers
func NewEventHandlers() *EventHandlers {
return &EventHandlers{
eventHandlers: make(map[EventHandler]struct{}),
}
}
// Handle callbacks for the events
func (eh *EventHandlers) Handle(e *Event) {
eh.RLock()
defer eh.RUnlock()
for h := range eh.eventHandlers {
if err := h.Handle(e); err != nil {
log.Error(err)
}
}
}
// RegisterEventHandler registers an event handler.
func (eh *EventHandlers) RegisterEventHandler(h EventHandler) error {
eh.Lock()
defer eh.Unlock()
if _, ok := eh.eventHandlers[h]; ok {
return errors.New("event handler already set")
}
eh.eventHandlers[h] = struct{}{}
return nil
}
// UnregisterEventHandler unregisters a previously registered event handler.
func (eh *EventHandlers) UnregisterEventHandler(h EventHandler) {
eh.Lock()
defer eh.Unlock()
delete(eh.eventHandlers, h)
}

85
cluster/event_monitor.go Normal file
View File

@@ -0,0 +1,85 @@
package cluster
import (
"encoding/json"
"io"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/events"
"github.com/docker/swarm/swarmclient"
"golang.org/x/net/context"
)
//EventsMonitor monitors events
type EventsMonitor struct {
stopChan chan struct{}
cli swarmclient.SwarmAPIClient
handler func(msg events.Message) error
}
type decodingResult struct {
msg events.Message
err error
}
// NewEventsMonitor returns an EventsMonitor
func NewEventsMonitor(cli swarmclient.SwarmAPIClient, handler func(msg events.Message) error) *EventsMonitor {
return &EventsMonitor{
cli: cli,
handler: handler,
}
}
// Start starts the EventsMonitor
func (em *EventsMonitor) Start(ec chan error) {
em.stopChan = make(chan struct{})
responseBody, err := em.cli.Events(context.Background(), types.EventsOptions{})
if err != nil {
ec <- err
return
}
resultChan := make(chan decodingResult)
go func() {
dec := json.NewDecoder(responseBody)
for {
var result decodingResult
result.err = dec.Decode(&result.msg)
resultChan <- result
if result.err == io.EOF {
break
}
}
close(resultChan)
}()
go func() {
defer responseBody.Close()
for {
select {
case <-em.stopChan:
ec <- nil
return
case result := <-resultChan:
if result.err != nil {
ec <- result.err
return
}
if err := em.handler(result.msg); err != nil {
ec <- err
return
}
}
}
}()
}
// Stop stops the EventsMonitor
func (em *EventsMonitor) Stop() {
if em.stopChan == nil {
return
}
close(em.stopChan)
}

57
cluster/httpclient.go Normal file
View File

@@ -0,0 +1,57 @@
package cluster
import (
"crypto/tls"
"net"
"net/http"
"net/url"
"time"
)
type tcpFunc func(*net.TCPConn, time.Duration) error
func newHTTPClient(u *url.URL, tlsConfig *tls.Config, timeout time.Duration, setUserTimeout tcpFunc) *http.Client {
httpTransport := &http.Transport{
TLSClientConfig: tlsConfig,
}
switch u.Scheme {
default:
httpTransport.Dial = func(proto, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(proto, addr, timeout)
if tcpConn, ok := conn.(*net.TCPConn); ok && setUserTimeout != nil {
// Sender can break TCP connection if the remote side doesn't
// acknowledge packets within timeout
setUserTimeout(tcpConn, timeout)
}
return conn, err
}
case "unix":
socketPath := u.Path
unixDial := func(proto, addr string) (net.Conn, error) {
return net.DialTimeout("unix", socketPath, timeout)
}
httpTransport.Dial = unixDial
// Override the main URL object so the HTTP lib won't complain
u.Scheme = "http"
u.Host = "unix.sock"
u.Path = ""
}
return &http.Client{Transport: httpTransport}
}
func NewHTTPClientTimeout(daemonUrl string, tlsConfig *tls.Config, timeout time.Duration, setUserTimeout tcpFunc) (*http.Client, *url.URL, error) {
u, err := url.Parse(daemonUrl)
if err != nil {
return nil, nil, err
}
if u.Scheme == "" || u.Scheme == "tcp" {
if tlsConfig == nil {
u.Scheme = "http"
} else {
u.Scheme = "https"
}
}
httpClient := newHTTPClient(u, tlsConfig, timeout, setUserTimeout)
return httpClient, u, nil
}

126
cluster/image.go Normal file
View File

@@ -0,0 +1,126 @@
package cluster
import (
"strings"
"github.com/docker/engine-api/types"
)
// Image is exported
type Image struct {
types.Image
Engine *Engine
}
// ParseRepositoryTag gets a repos name and returns the right reposName + tag|digest
// The tag can be confusing because of a port in a repository name.
// Ex: localhost.localdomain:5000/samalba/hipache:latest
// Digest ex: localhost:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb
func ParseRepositoryTag(repos string) (string, string) {
n := strings.Index(repos, "@")
if n >= 0 {
parts := strings.Split(repos, "@")
return parts[0], parts[1]
}
n = strings.LastIndex(repos, ":")
if n < 0 {
return repos, ""
}
if tag := repos[n+1:]; !strings.Contains(tag, "/") {
return repos[:n], tag
}
return repos, ""
}
// Match is exported
func (image *Image) Match(IDOrName string, matchTag bool) bool {
size := len(IDOrName)
// TODO: prefix match can cause false positives with image names
if image.ID == IDOrName || (size > 2 && strings.HasPrefix(image.ID, IDOrName)) {
return true
}
// trim sha256: and retry
if parts := strings.SplitN(image.ID, ":", 2); len(parts) == 2 {
if parts[1] == IDOrName || (size > 2 && strings.HasPrefix(parts[1], IDOrName)) {
return true
}
}
repoName, tag := ParseRepositoryTag(IDOrName)
// match repotag
for _, imageRepoTag := range image.RepoTags {
imageRepoName, imageTag := ParseRepositoryTag(imageRepoTag)
if matchTag == false && imageRepoName == repoName {
return true
}
if imageRepoName == repoName && (imageTag == tag || tag == "") {
return true
}
}
// match repodigests
for _, imageDigest := range image.RepoDigests {
imageRepoName, imageDigest := ParseRepositoryTag(imageDigest)
if matchTag == false && imageRepoName == repoName {
return true
}
if imageRepoName == repoName && (imageDigest == tag || tag == "") {
return true
}
}
return false
}
// ImageFilterOptions is the set of filtering options supported by
// Images.Filter()
type ImageFilterOptions struct {
types.ImageListOptions
}
// Images is a collection of Image objects that can be filtered
type Images []*Image
// Filter returns a new sequence of Images filtered to only the images that
// matched the filtering parameters
func (images Images) Filter(opts ImageFilterOptions) Images {
includeAll := func(image *Image) bool {
// TODO: this is wrong if RepoTags == []
return opts.All ||
(len(image.RepoTags) != 0 && image.RepoTags[0] != "<none>:<none>") ||
(len(image.RepoDigests) != 0 && image.RepoDigests[0] != "<none>@<none>")
}
includeFilter := func(image *Image) bool {
if opts.Filters.Len() == 0 {
return true
}
return opts.Filters.MatchKVList("label", image.Labels)
}
includeRepoFilter := func(image *Image) bool {
if opts.MatchName == "" {
return true
}
for _, repoTag := range image.RepoTags {
repoName, _ := ParseRepositoryTag(repoTag)
if repoTag == opts.MatchName || repoName == opts.MatchName {
return true
}
}
return false
}
filtered := make([]*Image, 0, len(images))
for _, image := range images {
if includeAll(image) && includeFilter(image) && includeRepoFilter(image) {
filtered = append(filtered, image)
}
}
return filtered
}

140
cluster/image_test.go Normal file
View File

@@ -0,0 +1,140 @@
package cluster
import (
"testing"
"github.com/docker/engine-api/types"
dockerfilters "github.com/docker/engine-api/types/filters"
"github.com/stretchr/testify/assert"
)
func TestMatch(t *testing.T) {
img := Image{}
img.ID = "378954456789"
img.RepoTags = []string{"name:latest"}
img.RepoDigests = []string{"name@sha256:a973f1415c489a934bf56dd653079d36b4ec717760215645726439de9705911d"}
assert.True(t, img.Match("378954456789", true))
assert.True(t, img.Match("3789", true))
assert.True(t, img.Match("378", true))
assert.False(t, img.Match("37", true))
assert.True(t, img.Match("name:latest", true))
assert.True(t, img.Match("name", true))
assert.False(t, img.Match("nam", true))
assert.False(t, img.Match("na", true))
assert.True(t, img.Match("378954456789", false))
assert.True(t, img.Match("3789", false))
assert.True(t, img.Match("378", false))
assert.False(t, img.Match("37", false))
assert.True(t, img.Match("name:latest", false))
assert.True(t, img.Match("name", false))
assert.False(t, img.Match("nam", false))
assert.False(t, img.Match("na", false))
assert.True(t, img.Match("name@sha256:a973f1415c489a934bf56dd653079d36b4ec717760215645726439de9705911d", true))
assert.False(t, img.Match("name@sha256:111111415c489a934bf56dd653079d36b4ec717760215645726439de9705911d", true))
}
func TestMatchPrivateRepo(t *testing.T) {
img := Image{}
img.ID = "378954456789"
img.RepoTags = []string{"private.registry.com:5000/name:latest"}
assert.True(t, img.Match("private.registry.com:5000/name:latest", true))
assert.True(t, img.Match("private.registry.com:5000/name", true))
assert.False(t, img.Match("private.registry.com:5000/nam", true))
assert.False(t, img.Match("private.registry.com:5000/na", true))
assert.True(t, img.Match("private.registry.com:5000/name", false))
assert.False(t, img.Match("private.registry.com:5000/nam", false))
assert.False(t, img.Match("private.registry.com:5000/na", false))
}
func TestImagesFilterWithLabelFilter(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
images := Images{
{types.Image{ID: "a"}, engine},
{types.Image{
ID: "b",
Labels: map[string]string{"com.example.project": "bar"},
}, engine},
{types.Image{ID: "c"}, engine},
}
filters := dockerfilters.NewArgs()
filters.Add("label", "com.example.project=bar")
result := images.Filter(ImageFilterOptions{types.ImageListOptions{All: true, Filters: filters}})
assert.Equal(t, len(result), 1)
assert.Equal(t, result[0].ID, "b")
}
func TestImagesFilterWithMatchName(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
images := Images{
{
types.Image{
ID: "a",
RepoTags: []string{"example:latest", "example:2"},
},
engine,
},
{
types.Image{ID: "b", RepoTags: []string{"example:1"}},
engine,
},
}
result := images.Filter(ImageFilterOptions{types.ImageListOptions{All: true, MatchName: "example:2"}})
assert.Equal(t, len(result), 1)
assert.Equal(t, result[0].ID, "a")
}
func TestImagesFilterWithMatchNameWithTag(t *testing.T) {
engine := NewEngine("test", 0, engOpts)
images := Images{
{
types.Image{
ID: "a",
RepoTags: []string{"example:latest", "example:2"},
},
engine,
},
{
types.Image{ID: "b", RepoTags: []string{"example:1"}},
engine,
},
{
types.Image{ID: "c", RepoTags: []string{"foo:latest"}},
engine,
},
}
result := images.Filter(ImageFilterOptions{types.ImageListOptions{All: true, MatchName: "example"}})
assert.Equal(t, len(result), 2)
}
func TestParseRepositoryTag(t *testing.T) {
repo, tag := ParseRepositoryTag("localhost.localdomain:5000/samalba/hipache:latest")
if tag != "latest" {
t.Errorf("repo=%s tag=%s", repo, tag)
}
repo, tag = ParseRepositoryTag("localhost:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb")
if tag != "sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb" {
t.Logf("repo=%s tag=%s", repo, tag)
}
repo, tag = ParseRepositoryTag("localhost:5000/foo/bar")
if tag != "" {
t.Logf("repo=%s tag=%s", repo, tag)
}
repo, tag = ParseRepositoryTag("localhost:5000/foo/bar:latest")
t.Logf("repo=%s tag=%s", repo, tag)
if tag != "latest" {
t.Logf("repo=%s tag=%s", repo, tag)
}
}

150
cluster/mesos/README.md Normal file
View File

@@ -0,0 +1,150 @@
# Using Docker Swarm and Mesos
Swarm comes with a built-in scheduler that works with the Swarm manager to
schedule container resources. You can completely replace the built-in scheduler
with a 3rd party scheduler. For example, you can replace it with the Mesos
scheduler as described here.
When using Docker Swarm and Mesos, you use the Docker client to ask the Swarm
manager to schedule containers. The Swarm manager then schedules those
containers on a Mesos cluster.
###### The Docker Swarm on Mesos integration is experimental and **not** production ready, please use with caution.
## Prerequisites
Each node in your Swarm must run a Mesos agent. The agent must be capable of
starting tasks in a Docker Container using the `--containerizers=docker` option.
You need to configure two TCP ports on the agent. One port to listen for the
Swarm manager, for example 2375. And a second TCP port to listen for the Mesos
master, for example 3375.
## Start the Docker Swarm manager
If you use a single Mesos master:
```
$ docker run -d -p <swarm_port>:2375 -p 3375:3375 \
swarm manage \
-c mesos-experimental \
--cluster-opt mesos.address=<public_machine_ip> \
--cluster-opt mesos.port=3375 \
<mesos_master_ip>:<mesos_master_port>
```
The command above creates a Swarm manager listening at `<swarm_port>`.
The `<mesos_master_ip>` value points to where Mesos master lives in the cluster.
Typically, this is localhost, a hostname, or an IP address. The `<public_machine_ip>`
value is the IP address for Mesos master to talk to Swarm manager. If Mesos master
and Swarm manager are co-located on the same machine, you can use the `0.0.0.0`
or `localhost` value.
If you use multiple Mesos masters:
```
$ docker run -d -p <swarm_port>:2375 -p 3375:3375 \
swarm manage \
-c mesos-experimental \
--cluster-opt mesos.address=<public_machine_ip> \
--cluster-opt mesos.port=3375 \
zk://<mesos_masters_url>
```
Once the manager is running, check your configuration by running `docker info`
as follows:
```
$ docker -H tcp://<swarm_ip>:<swarm_port> info
Containers: 0
Offers: 2
Offer: 20150609-222929-1327399946-5050-14390-O6286
└ cpus: 2
└ mem: 1006 MiB
└ disk: 34.37 GiB
└ ports: 31000-32000
Offer: 20150609-222929-1327399946-5050-14390-O6287
└ cpus: 2
└ mem: 1006 MiB
└ disk: 34.37 GiB
└ ports: 31000-32000
```
If you run into `Abnormal executor termination` error, you might want to run the
Swarm container with an additional environment variable:
`SWARM_MESOS_USER=root`.
Exporting the environment variable `DOCKER_HOST=<swarm_ip>:<swarm_port>` will facilitate
interaction with the Docker client as it will not require `-H` anymore.
# Limitations
Docker Swarm on Mesos provides a basic Mesos offers negotiation
It is also possible to use Docker-compose to communicate to the Docker Swarm manager how to schedule Docker containers on the Mesos cluster.
Docker Swarm on Mesos supports Mesos master detection with Zookeeper
## Functionality
Docker Swarm on Mesos supports the following subset of Docker CLI commands:
+ attach
+ build
+ commit
+ cp
+ diff
+ events
+ export
+ history
+ images
+ info
+ inspect
+ logs
+ network
* create
* remove
* ls
* inspect
* connect
* disconnect
+ port
+ ps
+ run
+ save
+ search
+ stats
+ top
## Unsupported docker commands
The current Swarm scheduler for Mesos implementation is not feature complete. Hence the following Docker CLI commands are unsupported and may cause unpredictable results:
+ create
+ import
+ kill
+ load
+ login
+ logout
+ pause
+ pull
+ push
+ rename
+ restart
+ rm
+ rmi
+ start
+ stop
+ tag
+ unpause
+ update
+ volume
+ wait
## Known issues
- Docker Swarm on Mesos only uses unreserved resources; and if Mesos offers reserved resources, the role info is also ignored which is rejected by Mesos master. See [here](https://github.com/docker/swarm/issues/1618) for a proposal on letting Docker Swarm on Mesos use both un-reserved/reserved resources.
- See [here](https://github.com/docker/swarm/issues/1619) for a proposal on letting Docker Swarm on Mesos use revocable resources.
- Restarted Mesos agents have flaky recovery in conjunction with Docker Swarm. When Mesos agents restart, Mesos master don’t always send offers for those agents to Docker Swarm. This issue seems to be solved with the Swarm 1.1 release.
To have a global view of the tracked issues please refer to
[Mesos issues in Github](https://github.com/docker/swarm/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+mesos).

79
cluster/mesos/agent.go Normal file
View File

@@ -0,0 +1,79 @@
package mesos
import (
"sync"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/cluster/mesos/task"
"github.com/mesos/mesos-go/mesosproto"
)
type agent struct {
sync.RWMutex
id string
offers map[string]*mesosproto.Offer
tasks map[string]*task.Task
engine *cluster.Engine
}
func newAgent(sid string, e *cluster.Engine) *agent {
return &agent{
id: sid,
offers: make(map[string]*mesosproto.Offer),
tasks: make(map[string]*task.Task),
engine: e,
}
}
func (s *agent) addOffer(offer *mesosproto.Offer) {
s.Lock()
s.offers[offer.Id.GetValue()] = offer
s.Unlock()
}
func (s *agent) addTask(task *task.Task) {
s.Lock()
s.tasks[task.TaskInfo.TaskId.GetValue()] = task
s.Unlock()
}
func (s *agent) removeOffer(offerID string) bool {
s.Lock()
defer s.Unlock()
found := false
_, found = s.offers[offerID]
if found {
delete(s.offers, offerID)
}
return found
}
func (s *agent) removeTask(taskID string) bool {
s.Lock()
defer s.Unlock()
found := false
_, found = s.tasks[taskID]
if found {
delete(s.tasks, taskID)
}
return found
}
func (s *agent) empty() bool {
s.RLock()
defer s.RUnlock()
return len(s.offers) == 0 && len(s.tasks) == 0
}
func (s *agent) getOffers() map[string]*mesosproto.Offer {
s.RLock()
defer s.RUnlock()
return s.offers
}
func (s *agent) getTasks() map[string]*task.Task {
s.RLock()
defer s.RUnlock()
return s.tasks
}

100
cluster/mesos/agent_test.go Normal file
View File

@@ -0,0 +1,100 @@
package mesos
import (
"testing"
"time"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/cluster/mesos/task"
"github.com/mesos/mesos-go/mesosutil"
"github.com/stretchr/testify/assert"
)
func TestNewAgent(t *testing.T) {
s := newAgent("SID", nil)
assert.Equal(t, s.id, "SID")
assert.Empty(t, s.offers)
assert.Empty(t, s.tasks)
assert.True(t, s.empty())
}
func TestAddOffer(t *testing.T) {
s := newAgent("SID", nil)
assert.Empty(t, s.offers)
assert.True(t, s.empty())
s.addOffer(mesosutil.NewOffer(mesosutil.NewOfferID("ID1"), nil, nil, "hostname1"))
s.addOffer(mesosutil.NewOffer(mesosutil.NewOfferID("ID2"), nil, nil, "hostname1"))
assert.Equal(t, len(s.offers), 2)
assert.Equal(t, len(s.getOffers()), 2)
assert.False(t, s.empty())
s.addOffer(mesosutil.NewOffer(mesosutil.NewOfferID("ID1"), nil, nil, "hostname1"))
assert.Equal(t, len(s.offers), 2)
assert.Equal(t, len(s.getOffers()), 2)
}
func TestAddTask(t *testing.T) {
s := newAgent("SID", nil)
assert.Empty(t, s.tasks)
assert.True(t, s.empty())
t1, err := task.NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), "task1", 5*time.Second)
assert.NoError(t, err)
s.addTask(t1)
t2, err := task.NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), "task1", 5*time.Second)
assert.NoError(t, err)
s.addTask(t2)
assert.Equal(t, len(s.tasks), 2)
assert.Equal(t, len(s.getTasks()), 2)
assert.False(t, s.empty())
s.addTask(t1)
assert.Equal(t, len(s.tasks), 2)
assert.Equal(t, len(s.getTasks()), 2)
}
func TestRemoveOffer(t *testing.T) {
s := newAgent("SID", nil)
assert.Empty(t, s.offers)
s.addOffer(mesosutil.NewOffer(mesosutil.NewOfferID("ID1"), nil, nil, "hostname1"))
s.addOffer(mesosutil.NewOffer(mesosutil.NewOfferID("ID2"), nil, nil, "hostname1"))
assert.Equal(t, len(s.offers), 2)
assert.Equal(t, len(s.getOffers()), 2)
assert.True(t, s.removeOffer("ID1"))
assert.Equal(t, len(s.offers), 1)
assert.Equal(t, len(s.getOffers()), 1)
assert.False(t, s.removeOffer("ID1"))
}
func TestRemoveTask(t *testing.T) {
s := newAgent("SID", nil)
assert.Empty(t, s.tasks)
t1, err := task.NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), "task1", 5*time.Second)
assert.NoError(t, err)
s.addTask(t1)
t2, err := task.NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), "task1", 5*time.Second)
assert.NoError(t, err)
s.addTask(t2)
assert.Equal(t, len(s.tasks), 2)
assert.Equal(t, len(s.getTasks()), 2)
assert.True(t, s.removeTask(t1.TaskId.GetValue()))
assert.Equal(t, len(s.tasks), 1)
assert.Equal(t, len(s.getTasks()), 1)
assert.False(t, s.removeTask(t1.TaskId.GetValue()))
}

719
cluster/mesos/cluster.go Normal file
View File

@@ -0,0 +1,719 @@
package mesos
import (
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/cluster/mesos/task"
"github.com/docker/swarm/scheduler"
"github.com/docker/swarm/scheduler/node"
"github.com/gogo/protobuf/proto"
"github.com/mesos/mesos-go/mesosproto"
mesosscheduler "github.com/mesos/mesos-go/scheduler"
"github.com/samalba/dockerclient"
)
// Cluster struct for mesos
type Cluster struct {
sync.RWMutex
dockerEnginePort string
eventHandlers *cluster.EventHandlers
master string
agents map[string]*agent
scheduler *Scheduler
TLSConfig *tls.Config
options *cluster.DriverOpts
offerTimeout time.Duration
refuseTimeout time.Duration
taskCreationTimeout time.Duration
pendingTasks *task.Tasks
engineOpts *cluster.EngineOpts
}
const (
frameworkName = "swarm"
defaultDockerEnginePort = "2375"
defaultDockerEngineTLSPort = "2376"
dockerPortAttribute = "docker_port"
defaultOfferTimeout = 30 * time.Second
defaultRefuseTimeout = 5 * time.Second
defaultTaskCreationTimeout = 5 * time.Second
)
var (
errNotSupported = errors.New("not supported with mesos")
errResourcesNeeded = errors.New("resources constraints (-c and/or -m) are required by mesos")
)
// NewCluster for mesos Cluster creation
func NewCluster(scheduler *scheduler.Scheduler, TLSConfig *tls.Config, master string, options cluster.DriverOpts, engineOptions *cluster.EngineOpts) (cluster.Cluster, error) {
log.WithFields(log.Fields{"name": "mesos"}).Debug("Initializing cluster")
// Enabling mesos-go glog logging
if log.GetLevel() == log.DebugLevel {
flag.Lookup("logtostderr").Value.Set("true")
}
cluster := &Cluster{
dockerEnginePort: defaultDockerEnginePort,
eventHandlers: cluster.NewEventHandlers(),
master: master,
agents: make(map[string]*agent),
TLSConfig: TLSConfig,
options: &options,
offerTimeout: defaultOfferTimeout,
taskCreationTimeout: defaultTaskCreationTimeout,
engineOpts: engineOptions,
refuseTimeout: defaultRefuseTimeout,
}
cluster.pendingTasks = task.NewTasks(cluster)
// Empty string is accepted by the scheduler.
user, _ := options.String("mesos.user", "SWARM_MESOS_USER")
// Override the hostname here because mesos-go will try
// to shell out to the hostname binary and it won't work with our official image.
// Do not check error here, so mesos-go can still try.
hostname, _ := os.Hostname()
driverConfig := mesosscheduler.DriverConfig{
Framework: &mesosproto.FrameworkInfo{Name: proto.String(frameworkName), User: &user},
Master: cluster.master,
HostnameOverride: hostname,
}
if taskCreationTimeout, ok := options.String("mesos.tasktimeout", "SWARM_MESOS_TASK_TIMEOUT"); ok {
d, err := time.ParseDuration(taskCreationTimeout)
if err != nil {
return nil, err
}
cluster.taskCreationTimeout = d
}
// Changing port for https
if cluster.TLSConfig != nil {
cluster.dockerEnginePort = defaultDockerEngineTLSPort
}
if bindingPort, ok := options.Uint("mesos.port", "SWARM_MESOS_PORT"); ok {
driverConfig.BindingPort = uint16(bindingPort)
}
if bindingAddress, ok := options.IP("mesos.address", "SWARM_MESOS_ADDRESS"); ok {
if bindingAddress == nil {
value, _ := options.String("mesos.address", "SWARM_MESOS_ADDRESS")
return nil, fmt.Errorf(
"invalid IP address for cluster-opt mesos.address: \"%s\"",
value)
}
driverConfig.BindingAddress = bindingAddress
}
if checkpointFailover, ok := options.Bool("mesos.checkpointfailover", "SWARM_MESOS_CHECKPOINT_FAILOVER"); ok {
driverConfig.Framework.Checkpoint = &checkpointFailover
}
if offerTimeout, ok := options.String("mesos.offertimeout", "SWARM_MESOS_OFFER_TIMEOUT"); ok {
d, err := time.ParseDuration(offerTimeout)
if err != nil {
return nil, err
}
cluster.offerTimeout = d
}
if refuseTimeout, ok := options.String("mesos.offerrefusetimeout", "SWARM_MESOS_OFFER_REFUSE_TIMEOUT"); ok {
d, err := time.ParseDuration(refuseTimeout)
if err != nil {
return nil, err
}
cluster.refuseTimeout = d
}
sched, err := NewScheduler(driverConfig, cluster, scheduler)
if err != nil {
return nil, err
}
cluster.scheduler = sched
status, err := sched.driver.Start()
if err != nil {
log.Debugf("Mesos driver started, status/err %v: %v", status, err)
return nil, err
}
log.Debugf("Mesos driver started, status %v", status)
go func() {
status, err := sched.driver.Join()
log.Debugf("Mesos driver stopped unexpectedly, status/err %v: %v", status, err)
}()
return cluster, nil
}
// Handle callbacks for the events
func (c *Cluster) Handle(e *cluster.Event) error {
c.eventHandlers.Handle(e)
return nil
}
// RegisterEventHandler registers an event handler.
func (c *Cluster) RegisterEventHandler(h cluster.EventHandler) error {
return c.eventHandlers.RegisterEventHandler(h)
}
// UnregisterEventHandler unregisters a previously registered event handler.
func (c *Cluster) UnregisterEventHandler(h cluster.EventHandler) {
c.eventHandlers.UnregisterEventHandler(h)
}
// StartContainer starts a container
func (c *Cluster) StartContainer(container *cluster.Container, hostConfig *dockerclient.HostConfig) error {
// if the container was started less than a second ago in detach mode, do not start it
if time.Now().Unix()-container.Created > 1 || container.Config.Labels[cluster.SwarmLabelNamespace+".mesos.detach"] != "true" {
return container.Engine.StartContainer(container.ID, hostConfig)
}
return nil
}
// CreateContainer for container creation in Mesos task
func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string, authConfig *types.AuthConfig) (*cluster.Container, error) {
if config.HostConfig.Memory == 0 && config.HostConfig.CPUShares == 0 {
return nil, errResourcesNeeded
}
if !c.checkNameUniqueness(name) {
return nil, fmt.Errorf("Conflict: The name %s is already assigned or in pending tasks. You have to delete (or rename) that container to be able to assign %s to a container again.", name, name)
}
task, err := task.NewTask(config, name, c.taskCreationTimeout)
if err != nil {
return nil, err
}
go c.pendingTasks.Add(task)
select {
case container := <-task.GetContainer():
return formatContainer(container), nil
case err := <-task.Error:
c.pendingTasks.Remove(task)
return nil, err
}
}
// RemoveContainer removes containers on mesos cluster
func (c *Cluster) RemoveContainer(container *cluster.Container, force, volumes bool) error {
c.scheduler.Lock()
defer c.scheduler.Unlock()
return container.Engine.RemoveContainer(container, force, volumes)
}
// Images returns all the images in the cluster.
func (c *Cluster) Images() cluster.Images {
c.RLock()
defer c.RUnlock()
out := []*cluster.Image{}
for _, s := range c.agents {
out = append(out, s.engine.Images()...)
}
return out
}
// Image returns an image with IdOrName in the cluster
func (c *Cluster) Image(IDOrName string) *cluster.Image {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
c.RLock()
defer c.RUnlock()
for _, s := range c.agents {
if image := s.engine.Image(IDOrName); image != nil {
return image
}
}
return nil
}
// RemoveImages removes images from the cluster
func (c *Cluster) RemoveImages(name string, force bool) ([]types.ImageDelete, error) {
return nil, errNotSupported
}
// CreateNetwork creates a network in the cluster
func (c *Cluster) CreateNetwork(name string, request *types.NetworkCreate) (*types.NetworkCreateResponse, error) {
var (
parts = strings.SplitN(name, "/", 2)
config = &cluster.ContainerConfig{}
)
if len(parts) == 2 {
// a node was specified, create the container only on this node
name = parts[1]
config = cluster.BuildContainerConfig(containertypes.Config{Env: []string{"constraint:node==" + parts[0]}}, containertypes.HostConfig{}, networktypes.NetworkingConfig{})
}
c.scheduler.Lock()
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
c.scheduler.Unlock()
if err != nil {
return nil, err
}
if nodes == nil {
return nil, errors.New("cannot find node to create network")
}
n := nodes[0]
s, ok := c.agents[n.ID]
if !ok {
return nil, fmt.Errorf("Unable to create network on agent %q", n.ID)
}
resp, err := s.engine.CreateNetwork(name, request)
c.refreshNetworks()
return resp, err
}
func (c *Cluster) refreshNetworks() {
var wg sync.WaitGroup
for _, s := range c.agents {
e := s.engine
wg.Add(1)
go func(e *cluster.Engine) {
e.RefreshNetworks()
wg.Done()
}(e)
}
wg.Wait()
}
// CreateVolume creates a volume in the cluster
func (c *Cluster) CreateVolume(request *types.VolumeCreateRequest) (*types.Volume, error) {
return nil, errNotSupported
}
// RemoveNetwork removes network from the cluster
func (c *Cluster) RemoveNetwork(network *cluster.Network) error {
err := network.Engine.RemoveNetwork(network)
c.refreshNetworks()
return err
}
// RemoveVolumes removes volumes from the cluster
func (c *Cluster) RemoveVolumes(name string) (bool, error) {
return false, errNotSupported
}
func formatContainer(container *cluster.Container) *cluster.Container {
if container == nil {
return nil
}
if name := container.Config.Labels[cluster.SwarmLabelNamespace+".mesos.name"]; name != "" && container.Names[0] != "/"+name {
container.Names = append([]string{"/" + name}, container.Names...)
}
return container
}
// Containers returns all the containers in the cluster.
func (c *Cluster) Containers() cluster.Containers {
c.RLock()
defer c.RUnlock()
out := cluster.Containers{}
for _, s := range c.agents {
for _, container := range s.engine.Containers() {
if container.Config.Labels != nil {
if _, ok := container.Config.Labels[cluster.SwarmLabelNamespace+".mesos.task"]; ok {
out = append(out, formatContainer(container))
}
}
}
}
return out
}
// Container returns the container with IdOrName in the cluster
func (c *Cluster) Container(IDOrName string) *cluster.Container {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
return formatContainer(cluster.Containers(c.Containers()).Get(IDOrName))
}
// RemoveImage removes an image from the cluster
func (c *Cluster) RemoveImage(image *cluster.Image) ([]types.ImageDelete, error) {
return nil, errNotSupported
}
// Pull pulls images on the cluster nodes
func (c *Cluster) Pull(name string, authConfig *types.AuthConfig, callback func(where, status string, err error)) {
}
// Load images
func (c *Cluster) Load(imageReader io.Reader, callback func(where, status string, err error)) {
}
// Import image
func (c *Cluster) Import(source string, ref string, tag string, imageReader io.Reader, callback func(what, status string, err error)) {
}
// RenameContainer renames a container
func (c *Cluster) RenameContainer(container *cluster.Container, newName string) error {
//FIXME this doesn't work as the next refreshcontainer will erase this change (this change is in-memory only)
if !c.checkNameUniqueness(newName) {
return fmt.Errorf("Conflict: The name %s is already assigned, or in pending tasks. You have to delete (or rename) that container to be able to assign %s to a container again.", newName, newName)
}
container.Config.Labels[cluster.SwarmLabelNamespace+".mesos.name"] = newName
return nil
}
// Networks returns all the networks in the cluster.
func (c *Cluster) Networks() cluster.Networks {
c.RLock()
defer c.RUnlock()
out := cluster.Networks{}
for _, s := range c.agents {
out = append(out, s.engine.Networks()...)
}
return out
}
// Volumes returns all the volumes in the cluster.
func (c *Cluster) Volumes() cluster.Volumes {
return nil
}
// listNodes returns all the nodes in the cluster.
func (c *Cluster) listNodes() []*node.Node {
c.RLock()
defer c.RUnlock()
out := []*node.Node{}
for _, s := range c.agents {
n := node.NewNode(s.engine)
n.ID = s.id
n.TotalCpus = int64(sumScalarResourceValue(s.offers, "cpus"))
n.UsedCpus = 0
n.TotalMemory = int64(sumScalarResourceValue(s.offers, "mem")) * 1024 * 1024
n.UsedMemory = 0
out = append(out, n)
}
return out
}
func (c *Cluster) listOffers() []*mesosproto.Offer {
c.RLock()
defer c.RUnlock()
list := []*mesosproto.Offer{}
for _, s := range c.agents {
for _, offer := range s.offers {
list = append(list, offer)
}
}
return list
}
// TotalMemory returns the total memory of the cluster
func (c *Cluster) TotalMemory() int64 {
c.RLock()
defer c.RUnlock()
var totalMemory int64
for _, s := range c.agents {
totalMemory += int64(sumScalarResourceValue(s.offers, "mem")) * 1024 * 1024
}
return totalMemory
}
// TotalCpus returns the total memory of the cluster
func (c *Cluster) TotalCpus() int64 {
c.RLock()
defer c.RUnlock()
var totalCpus int64
for _, s := range c.agents {
totalCpus += int64(sumScalarResourceValue(s.offers, "cpus"))
}
return totalCpus
}
// Info gives minimal information about containers and resources on the mesos cluster
func (c *Cluster) Info() [][2]string {
offers := c.listOffers()
info := [][2]string{
{"Strategy", c.scheduler.Strategy()},
{"Filters", c.scheduler.Filters()},
{"Offers", fmt.Sprintf("%d", len(offers))},
}
sort.Sort(offerSorter(offers))
for _, offer := range offers {
info = append(info, [2]string{" Offer", offer.Id.GetValue()})
for _, resource := range offer.Resources {
info = append(info, [2]string{" └ " + resource.GetName(), formatResource(resource)})
}
}
return info
}
func (c *Cluster) addOffer(offer *mesosproto.Offer) {
s, ok := c.agents[offer.SlaveId.GetValue()]
if !ok {
return
}
s.addOffer(offer)
go func(offer *mesosproto.Offer) {
time.Sleep(c.offerTimeout)
// declining Mesos offers to make them available to other Mesos services
if c.removeOffer(offer) {
if _, err := c.scheduler.driver.DeclineOffer(offer.Id, &mesosproto.Filters{}); err != nil {
log.WithFields(log.Fields{"name": "mesos"}).Errorf("Error while declining offer %q: %v", offer.Id.GetValue(), err)
} else {
log.WithFields(log.Fields{"name": "mesos"}).Debugf("Offer %q declined successfully", offer.Id.GetValue())
}
}
}(offer)
}
func (c *Cluster) removeOffer(offer *mesosproto.Offer) bool {
log.WithFields(log.Fields{"name": "mesos", "offerID": offer.Id.String()}).Debug("Removing offer")
s, ok := c.agents[offer.SlaveId.GetValue()]
if !ok {
return false
}
found := s.removeOffer(offer.Id.GetValue())
if s.empty() {
// Disconnect from engine
s.engine.Disconnect()
delete(c.agents, offer.SlaveId.GetValue())
}
return found
}
// LaunchTask selects node and calls driver to launch a task
func (c *Cluster) LaunchTask(t *task.Task) bool {
c.scheduler.Lock()
//change to explicit lock defer c.scheduler.Unlock()
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), t.GetConfig())
if err != nil {
c.scheduler.Unlock()
return false
}
n := nodes[0]
s, ok := c.agents[n.ID]
if !ok {
t.Error <- fmt.Errorf("Unable to create on agent %q", n.ID)
c.scheduler.Unlock()
return true
}
// build the offer from its internal config and set the agentID
c.Lock()
// TODO: Only use the offer we need
offerIDs := []*mesosproto.OfferID{}
for _, offer := range c.agents[n.ID].offers {
offerIDs = append(offerIDs, offer.Id)
}
t.Build(n.ID, c.agents[n.ID].offers)
offerFilters := &mesosproto.Filters{}
refuseSeconds := c.refuseTimeout.Seconds()
offerFilters.RefuseSeconds = &refuseSeconds
if _, err := c.scheduler.driver.LaunchTasks(offerIDs, []*mesosproto.TaskInfo{&t.TaskInfo}, offerFilters); err != nil {
// TODO: Do not erase all the offers, only the one used
for _, offer := range s.offers {
c.removeOffer(offer)
}
c.Unlock()
c.scheduler.Unlock()
t.Error <- err
return true
}
s.addTask(t)
// TODO: Do not erase all the offers, only the one used
for _, offer := range s.offers {
c.removeOffer(offer)
}
c.Unlock()
c.scheduler.Unlock()
// block until we get the container
finished, data, err := t.Monitor()
taskID := t.TaskInfo.TaskId.GetValue()
if err != nil {
//remove task
s.removeTask(taskID)
t.Error <- err
return true
}
if !finished {
go func() {
for {
finished, _, err := t.Monitor()
if err != nil {
// TODO do a better log by sending proper error message
log.Error(err)
break
}
if finished {
break
}
}
//remove the task once it's finished
}()
}
// Register the container immediately while waiting for a state refresh.
// In mesos 0.23+ the docker inspect will be sent back in the taskStatus.Data
// We can use this to find the right container.
inspect := []types.ContainerJSONBase{}
if data != nil && json.Unmarshal(data, &inspect) == nil && len(inspect) == 1 {
container := &cluster.Container{Container: types.Container{ID: inspect[0].ID}, Engine: s.engine}
if container, err := container.Refresh(); err == nil {
if !t.Stopped() {
t.SetContainer(container)
}
return true
}
}
log.Debug("Cannot parse docker info from task status, please upgrade Mesos to the latest version")
// For mesos <= 0.22 we fallback to a full refresh + using labels
// TODO: once 0.23 or 0.24 is released, remove all this block of code as it
// doesn't scale very well.
s.engine.RefreshContainers(true)
for _, container := range s.engine.Containers() {
if container.Config.Labels[cluster.SwarmLabelNamespace+".mesos.task"] == taskID {
if !t.Stopped() {
t.SetContainer(container)
}
return true
}
}
if !t.Stopped() {
t.Error <- fmt.Errorf("Container failed to create")
}
return true
}
// RANDOMENGINE returns a random engine.
func (c *Cluster) RANDOMENGINE() (*cluster.Engine, error) {
c.RLock()
defer c.RUnlock()
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), &cluster.ContainerConfig{})
if err != nil {
return nil, err
}
n := nodes[0]
return c.agents[n.ID].engine, nil
}
// BuildImage builds an image
func (c *Cluster) BuildImage(buildContext io.Reader, buildImage *types.ImageBuildOptions, out io.Writer) error {
c.scheduler.Lock()
// get an engine
config := &cluster.ContainerConfig{
HostConfig: containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: buildImage.CPUShares,
Memory: buildImage.Memory,
},
},
}
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
c.scheduler.Unlock()
if err != nil {
return err
}
n := nodes[0]
reader, err := c.agents[n.ID].engine.BuildImage(buildContext, buildImage)
if err != nil {
return err
}
if _, err := io.Copy(out, reader); err != nil {
return err
}
c.agents[n.ID].engine.RefreshImages()
return nil
}
// TagImage tags an image
func (c *Cluster) TagImage(IDOrName string, ref string, force bool) error {
return errNotSupported
}
func (c *Cluster) checkNameUniqueness(name string) bool {
// Abort immediately if the name is empty.
if len(name) == 0 {
return true
}
c.RLock()
defer c.RUnlock()
for _, s := range c.agents {
for _, container := range s.engine.Containers() {
for _, cname := range container.Names {
if cname == name || cname == "/"+name {
return false
}
}
}
}
for _, task := range c.pendingTasks.Tasks {
config := task.GetConfig()
if config.Labels != nil {
if tname, ok := config.Labels[cluster.SwarmLabelNamespace+".mesos.name"]; ok {
if tname == name || tname == "/"+name {
return false
}
}
}
}
return true
}

View File

@@ -0,0 +1,95 @@
package mesos
import (
"testing"
"time"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/docker/swarm/cluster"
"github.com/stretchr/testify/assert"
)
func createAgent(t *testing.T, ID string, containers ...*cluster.Container) *agent {
engOpts := &cluster.EngineOpts{
RefreshMinInterval: time.Duration(30) * time.Second,
RefreshMaxInterval: time.Duration(60) * time.Second,
FailureRetry: 3,
}
engine := cluster.NewEngine(ID, 0, engOpts)
engine.Name = ID
engine.ID = ID
for _, container := range containers {
container.Engine = engine
engine.AddContainer(container)
}
return newAgent("agent-"+ID, engine)
}
func TestContainerLookup(t *testing.T) {
c := &Cluster{
agents: make(map[string]*agent),
}
container1 := &cluster.Container{
Container: types.Container{
ID: "container1-id",
Names: []string{"/container1-name1", "/container1-name2"},
},
Config: cluster.BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.mesos.task": "task1-id",
"com.docker.swarm.mesos.name": "container1-name",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}
container2 := &cluster.Container{
Container: types.Container{
ID: "container2-id",
Names: []string{"/con"},
},
Config: cluster.BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.mesos.task": "task2-id",
"com.docker.swarm.mesos.name": "con",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}
container3 := &cluster.Container{
Container: types.Container{
ID: "container3-id",
Names: []string{"/container3-name"},
},
Config: cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}
s := createAgent(t, "test-engine", container1, container2, container3)
c.agents[s.id] = s
// Hide container without `com.docker.swarm.mesos.task`
assert.Equal(t, len(c.Containers()), 2)
// Invalid lookup
assert.Nil(t, c.Container("invalid-id"))
assert.Nil(t, c.Container(""))
// Container ID lookup.
assert.NotNil(t, c.Container("container1-id"))
// Container ID prefix lookup.
assert.NotNil(t, c.Container("container1-"))
assert.Nil(t, c.Container("container"))
// Container name lookup.
assert.NotNil(t, c.Container("container1-name1"))
assert.NotNil(t, c.Container("container1-name2"))
// Container engine/name matching.
assert.NotNil(t, c.Container("test-engine/container1-name1"))
assert.NotNil(t, c.Container("test-engine/container1-name2"))
// Match name before ID prefix
cc := c.Container("con")
assert.NotNil(t, cc)
assert.Equal(t, cc.ID, "container2-id")
}

View File

@@ -0,0 +1,23 @@
package mesos
import "github.com/mesos/mesos-go/mesosproto"
// OfferSorter implements the Sort interface to sort offers.
// It is not guaranteed to be a stable sort.
type offerSorter []*mesosproto.Offer
// Len returns the number of engines to be sorted.
func (s offerSorter) Len() int {
return len(s)
}
// Swap exchanges the engine elements with indices i and j.
func (s offerSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the engine with index i should sort before the engine with index j.
// Offers are sorted chronologically by name.
func (s offerSorter) Less(i, j int) bool {
return s[i].Id.GetValue() < s[j].Id.GetValue()
}

View File

@@ -0,0 +1,24 @@
package mesos
import (
"sort"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/mesos/mesos-go/mesosproto"
"github.com/stretchr/testify/assert"
)
func TestOfferSorter(t *testing.T) {
offers := []*mesosproto.Offer{
{Id: &mesosproto.OfferID{Value: proto.String("id1")}},
{Id: &mesosproto.OfferID{Value: proto.String("id3")}},
{Id: &mesosproto.OfferID{Value: proto.String("id2")}},
}
sort.Sort(offerSorter(offers))
assert.Equal(t, offers[0].Id.GetValue(), "id1")
assert.Equal(t, offers[1].Id.GetValue(), "id2")
assert.Equal(t, offers[2].Id.GetValue(), "id3")
}

146
cluster/mesos/scheduler.go Normal file
View File

@@ -0,0 +1,146 @@
package mesos
import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler"
"github.com/mesos/mesos-go/mesosproto"
mesosscheduler "github.com/mesos/mesos-go/scheduler"
)
// Scheduler structure for mesos driver
type Scheduler struct {
scheduler.Scheduler
driver *mesosscheduler.MesosSchedulerDriver
cluster *Cluster
}
// NewScheduler for Scheduler mesos driver creation
func NewScheduler(config mesosscheduler.DriverConfig, cluster *Cluster, sched *scheduler.Scheduler) (*Scheduler, error) {
scheduler := Scheduler{
Scheduler: *sched,
cluster: cluster,
}
config.Scheduler = &scheduler
driver, err := mesosscheduler.NewMesosSchedulerDriver(config)
if err != nil {
return nil, err
}
scheduler.driver = driver
return &scheduler, nil
}
// Registered method for registered mesos framework
func (s *Scheduler) Registered(driver mesosscheduler.SchedulerDriver, fwID *mesosproto.FrameworkID, masterInfo *mesosproto.MasterInfo) {
log.WithFields(log.Fields{"name": "mesos", "frameworkId": fwID.GetValue()}).Debug("Framework registered")
}
// Reregistered method for registered mesos framework
func (s *Scheduler) Reregistered(mesosscheduler.SchedulerDriver, *mesosproto.MasterInfo) {
log.WithFields(log.Fields{"name": "mesos"}).Debug("Framework re-registered")
}
// Disconnected method
func (s *Scheduler) Disconnected(mesosscheduler.SchedulerDriver) {
log.WithFields(log.Fields{"name": "mesos"}).Debug("Framework disconnected")
}
// ResourceOffers method
func (s *Scheduler) ResourceOffers(_ mesosscheduler.SchedulerDriver, offers []*mesosproto.Offer) {
log.WithFields(log.Fields{"name": "mesos", "offers": len(offers)}).Debug("Offers received")
for _, offer := range offers {
agentID := offer.SlaveId.GetValue()
dockerPort := s.cluster.dockerEnginePort
for _, attribute := range offer.GetAttributes() {
if attribute.GetName() == dockerPortAttribute {
switch attribute.GetType() {
case mesosproto.Value_SCALAR:
dockerPort = fmt.Sprintf("%d", int(attribute.GetScalar().GetValue()))
case mesosproto.Value_TEXT:
dockerPort = attribute.GetText().GetValue()
}
}
}
a, ok := s.cluster.agents[agentID]
if !ok {
engine := cluster.NewEngine(*offer.Hostname+":"+dockerPort, 0, s.cluster.engineOpts)
if err := engine.Connect(s.cluster.TLSConfig); err != nil {
log.Error(err)
} else {
// Set engine state to healthy and start refresh loop
engine.ValidationComplete()
a = newAgent(agentID, engine)
s.cluster.agents[agentID] = a
if err := a.engine.RegisterEventHandler(s.cluster); err != nil {
log.Error(err)
}
}
}
s.cluster.addOffer(offer)
}
go s.cluster.pendingTasks.Process()
}
// OfferRescinded method
func (s *Scheduler) OfferRescinded(_ mesosscheduler.SchedulerDriver, offerID *mesosproto.OfferID) {
log.WithFields(log.Fields{"name": "mesos", "OfferID": offerID.GetValue()}).Debug("Offer Rescinded")
for _, agent := range s.cluster.agents {
if offer, ok := agent.offers[offerID.GetValue()]; ok {
s.cluster.removeOffer(offer)
break
}
}
}
// StatusUpdate method
func (s *Scheduler) StatusUpdate(_ mesosscheduler.SchedulerDriver, taskStatus *mesosproto.TaskStatus) {
log.WithFields(log.Fields{"name": "mesos", "state": taskStatus.State.String()}).Debug("Status update")
taskID := taskStatus.TaskId.GetValue()
agentID := taskStatus.SlaveId.GetValue()
a, ok := s.cluster.agents[agentID]
if !ok {
return
}
if task, ok := a.tasks[taskID]; ok {
task.SendStatus(taskStatus)
} else {
var reason = ""
if taskStatus.Reason != nil {
reason = taskStatus.GetReason().String()
}
log.WithFields(log.Fields{
"name": "mesos",
"state": taskStatus.State.String(),
"agentId": taskStatus.SlaveId.GetValue(),
"reason": reason,
}).Warn("Status update received for unknown agent")
}
}
// FrameworkMessage method
func (s *Scheduler) FrameworkMessage(mesosscheduler.SchedulerDriver, *mesosproto.ExecutorID, *mesosproto.SlaveID, string) {
}
// SlaveLost method
func (s *Scheduler) SlaveLost(mesosscheduler.SchedulerDriver, *mesosproto.SlaveID) {
}
// ExecutorLost method
func (s *Scheduler) ExecutorLost(mesosscheduler.SchedulerDriver, *mesosproto.ExecutorID, *mesosproto.SlaveID, int) {
}
// Error method
func (s *Scheduler) Error(d mesosscheduler.SchedulerDriver, msg string) {
log.WithFields(log.Fields{"name": "mesos"}).Error(msg)
}

239
cluster/mesos/task/task.go Normal file
View File

@@ -0,0 +1,239 @@
package task
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/swarm/cluster"
"github.com/gogo/protobuf/proto"
"github.com/mesos/mesos-go/mesosproto"
"github.com/mesos/mesos-go/mesosutil"
)
// Task struct inherits from TaskInfo and represents a mesos task
type Task struct {
mesosproto.TaskInfo
updates chan *mesosproto.TaskStatus
config *cluster.ContainerConfig
Error chan error
container chan *cluster.Container
done bool
}
// GetContainer returns the container channel from the task
// where the Swarm API sends the created container
func (t *Task) GetContainer() chan *cluster.Container {
return t.container
}
// SetContainer writes on the container channel from the task
func (t *Task) SetContainer(container *cluster.Container) {
t.container <- container
}
// GetConfig returns the container configuration of the task
func (t *Task) GetConfig() *cluster.ContainerConfig {
return t.config
}
// ID method returns the taskId
func (t *Task) ID() string {
return t.TaskId.GetValue()
}
// Stopped method returns a boolean determining if the task
// is done
func (t *Task) Stopped() bool {
return t.done
}
// Stop method sets the boolean determining if the task is done
func (t *Task) Stop() {
t.done = true
}
// Build method builds the task
func (t *Task) Build(slaveID string, offers map[string]*mesosproto.Offer) {
t.Command = &mesosproto.CommandInfo{Shell: proto.Bool(false)}
t.Container = &mesosproto.ContainerInfo{
Type: mesosproto.ContainerInfo_DOCKER.Enum(),
Docker: &mesosproto.ContainerInfo_DockerInfo{
Image: &t.config.Image,
},
}
if t.config.Hostname != "" {
t.Container.Hostname = proto.String(t.config.Hostname)
if t.config.Domainname != "" {
t.Container.Hostname = proto.String(t.config.Hostname + "." + t.config.Domainname)
}
}
switch t.config.HostConfig.NetworkMode {
case "none":
t.Container.Docker.Network = mesosproto.ContainerInfo_DockerInfo_NONE.Enum()
case "host":
t.Container.Docker.Network = mesosproto.ContainerInfo_DockerInfo_HOST.Enum()
case "default", "bridge", "":
var ports []uint64
for _, offer := range offers {
ports = append(ports, getPorts(offer)...)
}
for containerProtoPort, bindings := range t.config.HostConfig.PortBindings {
for _, binding := range bindings {
containerInfo := strings.SplitN(string(containerProtoPort), "/", 2)
containerPort, err := strconv.ParseUint(containerInfo[0], 10, 32)
if err != nil {
log.Warn(err)
continue
}
var hostPort uint64
if binding.HostPort != "" {
hostPort, err = strconv.ParseUint(binding.HostPort, 10, 32)
if err != nil {
log.Warn(err)
continue
}
} else if len(ports) > 0 {
hostPort = ports[0]
ports = ports[1:]
}
if hostPort == 0 {
log.Warn("cannot find port to bind on the host")
continue
}
protocol := "tcp"
if len(containerInfo) == 2 {
protocol = containerInfo[1]
}
t.Container.Docker.PortMappings = append(t.Container.Docker.PortMappings, &mesosproto.ContainerInfo_DockerInfo_PortMapping{
HostPort: proto.Uint32(uint32(hostPort)),
ContainerPort: proto.Uint32(uint32(containerPort)),
Protocol: proto.String(protocol),
})
t.Resources = append(t.Resources, mesosutil.NewRangesResource("ports", []*mesosproto.Value_Range{mesosutil.NewValueRange(hostPort, hostPort)}))
}
}
// TODO handle -P here
t.Container.Docker.Network = mesosproto.ContainerInfo_DockerInfo_BRIDGE.Enum()
default:
log.Errorf("Unsupported network mode %q", t.config.HostConfig.NetworkMode)
t.Container.Docker.Network = mesosproto.ContainerInfo_DockerInfo_BRIDGE.Enum()
}
if cpus := t.config.HostConfig.CPUShares; cpus > 0 {
t.Resources = append(t.Resources, mesosutil.NewScalarResource("cpus", float64(cpus)))
}
if mem := t.config.HostConfig.Memory; mem > 0 {
t.Resources = append(t.Resources, mesosutil.NewScalarResource("mem", float64(mem/1024/1024)))
}
if len(t.config.Cmd) > 0 && t.config.Cmd[0] != "" {
t.Command.Value = &t.config.Cmd[0]
}
if len(t.config.Cmd) > 1 {
t.Command.Arguments = t.config.Cmd[1:]
}
for key, value := range t.config.Labels {
t.Container.Docker.Parameters = append(t.Container.Docker.Parameters, &mesosproto.Parameter{Key: proto.String("label"), Value: proto.String(fmt.Sprintf("%s=%s", key, value))})
}
for _, value := range t.config.Env {
t.Container.Docker.Parameters = append(t.Container.Docker.Parameters, &mesosproto.Parameter{Key: proto.String("env"), Value: proto.String(value)})
}
if !t.config.AttachStdin && !t.config.AttachStdout && !t.config.AttachStderr {
t.Container.Docker.Parameters = append(t.Container.Docker.Parameters, &mesosproto.Parameter{Key: proto.String("label"), Value: proto.String(fmt.Sprintf("%s=true", cluster.SwarmLabelNamespace+".mesos.detach"))})
}
t.SlaveId = &mesosproto.SlaveID{Value: &slaveID}
}
// NewTask function creates a task
func NewTask(config *cluster.ContainerConfig, name string, timeout time.Duration) (*Task, error) {
id := stringid.TruncateID(stringid.GenerateRandomID())
if name != "" {
id = name + "." + id
}
// save the name in labels as the mesos containerizer will override it
config.Labels[cluster.SwarmLabelNamespace+".mesos.name"] = name
// FIXME: once Mesos changes merged no need to save the task id to know which container we launched
config.Labels[cluster.SwarmLabelNamespace+".mesos.task"] = id
task := &Task{
config: config,
container: make(chan *cluster.Container),
Error: make(chan error),
updates: make(chan *mesosproto.TaskStatus),
}
task.Name = &name
task.TaskId = &mesosproto.TaskID{Value: &id}
task.Labels = &mesosproto.Labels{Labels: []*mesosproto.Label{{Key: proto.String("SWARM_CONTAINER_NAME"), Value: &name}}}
go task.suicide(timeout)
return task, nil
}
func (t *Task) suicide(timeout time.Duration) {
<-time.After(timeout)
if !t.Stopped() && t.SlaveId == nil {
t.Error <- fmt.Errorf("container failed to start after %s", timeout)
}
}
// SendStatus method writes the task status in the updates channel
func (t *Task) SendStatus(status *mesosproto.TaskStatus) {
t.updates <- status
}
// GetStatus method reads the task status on the updates channel
func (t *Task) GetStatus() *mesosproto.TaskStatus {
return <-t.updates
}
// Monitor method monitors task statuses
func (t *Task) Monitor() (bool, []byte, error) {
taskStatus := t.GetStatus()
switch taskStatus.GetState() {
case mesosproto.TaskState_TASK_STAGING:
case mesosproto.TaskState_TASK_STARTING:
case mesosproto.TaskState_TASK_RUNNING:
case mesosproto.TaskState_TASK_FINISHED:
return true, taskStatus.Data, nil
case mesosproto.TaskState_TASK_FAILED:
errorMessage := taskStatus.GetMessage()
if strings.Contains(errorMessage, "Abnormal executor termination") {
errorMessage += " : please verify your SWARM_MESOS_USER is correctly set"
}
return true, nil, errors.New(errorMessage)
case mesosproto.TaskState_TASK_KILLED:
return true, taskStatus.Data, nil
case mesosproto.TaskState_TASK_LOST:
return true, nil, errors.New(taskStatus.GetMessage())
case mesosproto.TaskState_TASK_ERROR:
return true, nil, errors.New(taskStatus.GetMessage())
}
return false, taskStatus.Data, nil
}

View File

@@ -0,0 +1,79 @@
package task
import (
"sort"
"strings"
"testing"
"time"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/docker/swarm/cluster"
"github.com/mesos/mesos-go/mesosproto"
"github.com/mesos/mesos-go/mesosutil"
"github.com/stretchr/testify/assert"
)
const name = "mesos-swarm-task-name"
var (
containerConfig = containertypes.Config{
Image: "test-image",
Cmd: []string{"ls", "foo", "bar"},
}
hostConfig = containertypes.HostConfig{
Resources: containertypes.Resources{
CPUShares: 42,
Memory: 2097152,
},
}
networkingConfig = networktypes.NetworkingConfig{}
)
func TestBuild(t *testing.T) {
task, err := NewTask(cluster.BuildContainerConfig(containerConfig, hostConfig, networkingConfig), name, 5*time.Second)
assert.NoError(t, err)
task.Build("slave-id", nil)
assert.Equal(t, task.Container.GetType(), mesosproto.ContainerInfo_DOCKER)
assert.Equal(t, task.Container.Docker.GetImage(), "test-image")
assert.Equal(t, task.Container.Docker.GetNetwork(), mesosproto.ContainerInfo_DockerInfo_BRIDGE)
assert.Equal(t, len(task.Resources), 2)
assert.Equal(t, task.Resources[0], mesosutil.NewScalarResource("cpus", 42.0))
assert.Equal(t, task.Resources[1], mesosutil.NewScalarResource("mem", 2))
assert.Equal(t, task.Command.GetValue(), "ls")
assert.Equal(t, task.Command.GetArguments(), []string{"foo", "bar"})
parameters := []string{task.Container.Docker.GetParameters()[0].GetValue(), task.Container.Docker.GetParameters()[1].GetValue()}
sort.Strings(parameters)
assert.Equal(t, len(parameters), 2)
assert.Equal(t, parameters[0], "com.docker.swarm.mesos.name="+name)
assert.Equal(t, parameters[1], "com.docker.swarm.mesos.task="+*task.TaskId.Value)
assert.Equal(t, task.SlaveId.GetValue(), "slave-id")
}
func TestNewTask(t *testing.T) {
task, err := NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), name, 5*time.Second)
assert.NoError(t, err)
assert.Equal(t, *task.Name, name)
assert.True(t, strings.HasPrefix(task.TaskId.GetValue(), name+"."))
assert.Equal(t, len(task.TaskId.GetValue()), len(name)+1+12) //<name>+.+<shortId>
}
func TestSendGetStatus(t *testing.T) {
task, err := NewTask(cluster.BuildContainerConfig(containertypes.Config{}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}), "", 5*time.Second)
assert.NoError(t, err)
status := mesosutil.NewTaskStatus(nil, mesosproto.TaskState_TASK_RUNNING)
go func() { task.SendStatus(status) }()
s := task.GetStatus()
assert.Equal(t, s, status)
}

View File

@@ -0,0 +1,61 @@
package task
import "sync"
type launcher interface {
LaunchTask(t *Task) bool
}
// Tasks is a simple map of tasks
type Tasks struct {
sync.Mutex
cluster launcher
Tasks map[string]*Task
}
// NewTasks returns a new tasks
func NewTasks(cluster launcher) *Tasks {
return &Tasks{
Tasks: make(map[string]*Task),
cluster: cluster,
}
}
// Add tries to Do the Task, if it's not possible, add the Task to the tasks for future tries
func (t *Tasks) Add(task *Task) {
if !t.cluster.LaunchTask(task) {
t.Lock()
t.Tasks[task.ID()] = task
t.Unlock()
}
}
// Remove a Task from the tasks
func (t *Tasks) Remove(tasks ...*Task) {
t.Lock()
t.remove(tasks...)
t.Unlock()
}
// Process tries to Do all the Tasks in the tasks and remove the Tasks successfully done
func (t *Tasks) Process() {
t.Lock()
toRemove := []*Task{}
for _, task := range t.Tasks {
if t.cluster.LaunchTask(task) {
toRemove = append(toRemove, task)
}
}
t.remove(toRemove...)
t.Unlock()
}
func (t *Tasks) remove(tasks ...*Task) {
for _, task := range tasks {
task.Stop()
delete(t.Tasks, task.ID())
}
}

View File

@@ -0,0 +1,56 @@
package task
import (
"testing"
"time"
"github.com/docker/swarm/cluster"
"github.com/stretchr/testify/assert"
)
type testLauncher struct {
count int
}
func (t *testLauncher) LaunchTask(_ *Task) bool {
t.count = t.count - 1
return t.count == 0
}
func TestAdd(t *testing.T) {
q := NewTasks(&testLauncher{count: 1})
task1, _ := NewTask(cluster.BuildContainerConfig(containerConfig, hostConfig, networkingConfig), "name1", 5*time.Second)
task2, _ := NewTask(cluster.BuildContainerConfig(containerConfig, hostConfig, networkingConfig), "name2", 5*time.Second)
q.Add(task1)
assert.Equal(t, len(q.Tasks), 0)
q.Add(task2)
assert.Equal(t, len(q.Tasks), 1)
}
func TestRemove(t *testing.T) {
q := NewTasks(&testLauncher{count: 2})
task1, _ := NewTask(cluster.BuildContainerConfig(containerConfig, hostConfig, networkingConfig), "name1", 5*time.Second)
q.Add(task1)
assert.Equal(t, len(q.Tasks), 1)
q.Remove(task1)
assert.Equal(t, len(q.Tasks), 0)
}
func TestProcess(t *testing.T) {
q := NewTasks(&testLauncher{count: 3})
task1, _ := NewTask(cluster.BuildContainerConfig(containerConfig, hostConfig, networkingConfig), "name1", 5*time.Second)
q.Add(task1)
assert.Equal(t, len(q.Tasks), 1)
q.Process()
assert.Equal(t, len(q.Tasks), 1)
q.Process()
assert.Equal(t, len(q.Tasks), 0)
}

View File

@@ -0,0 +1,16 @@
package task
import "github.com/mesos/mesos-go/mesosproto"
func getPorts(offer *mesosproto.Offer) (ports []uint64) {
for _, resource := range offer.Resources {
if resource.GetName() == "ports" {
for _, rang := range resource.GetRanges().GetRange() {
for i := rang.GetBegin(); i <= rang.GetEnd(); i++ {
ports = append(ports, i)
}
}
}
}
return ports
}

39
cluster/mesos/utils.go Normal file
View File

@@ -0,0 +1,39 @@
package mesos
import (
"fmt"
"strings"
"github.com/docker/go-units"
"github.com/mesos/mesos-go/mesosproto"
)
func formatResource(resource *mesosproto.Resource) string {
switch resource.GetType() {
case mesosproto.Value_SCALAR:
if resource.GetName() == "disk" || resource.GetName() == "mem" {
return units.BytesSize(resource.GetScalar().GetValue() * 1024 * 1024)
}
return fmt.Sprintf("%d", int(resource.GetScalar().GetValue()))
case mesosproto.Value_RANGES:
var ranges []string
for _, r := range resource.GetRanges().GetRange() {
ranges = append(ranges, fmt.Sprintf("%d-%d", r.GetBegin(), r.GetEnd()))
}
return strings.Join(ranges, ", ")
}
return "?"
}
func sumScalarResourceValue(offers map[string]*mesosproto.Offer, name string) float64 {
var value float64
for _, offer := range offers {
for _, resource := range offer.Resources {
if *resource.Name == name {
value += *resource.Scalar.Value
}
}
}
return value
}

163
cluster/network.go Normal file
View File

@@ -0,0 +1,163 @@
package cluster
import (
"strings"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/engine-api/types"
)
// Network is exported
type Network struct {
types.NetworkResource
Engine *Engine
}
func (network *Network) isPreDefined() bool {
return (network.Name == "none" || network.Name == "host" || network.Name == "bridge")
}
// Networks represents an array of networks
type Networks []*Network
// Uniq returns all uniq networks
func (networks Networks) Uniq() Networks {
tmp := make(map[string]*Network)
for _, network := range networks {
if _, ok := tmp[network.ID]; ok {
for id, endpoint := range network.Containers {
tmp[network.ID].Containers[id] = endpoint
}
} else {
netCopy := *network
netCopy.Containers = make(map[string]types.EndpointResource)
for key, value := range network.Containers {
netCopy.Containers[key] = value
}
tmp[network.ID] = &netCopy
}
}
uniq := Networks{}
for _, network := range tmp {
uniq = append(uniq, network)
}
return uniq
}
// Filter returns networks filtered by names or ids
func (networks Networks) Filter(names []string, ids []string, types []string) Networks {
typeFilter := func(network *Network) bool {
if len(types) > 0 {
for _, typ := range types {
if typ == "custom" && !network.isPreDefined() {
return true
}
if typ == "builtin" && network.isPreDefined() {
return true
}
}
} else {
return true
}
return false
}
out := Networks{}
if len(names) == 0 && len(ids) == 0 {
for _, network := range networks.Uniq() {
if typeFilter(network) {
out = append(out, network)
}
}
} else {
for _, idOrName := range append(names, ids...) {
if network := networks.Get(idOrName); network != nil {
if typeFilter(network) {
out = append(out, network)
}
}
}
}
return out
}
// RemoveDuplicateEndpoints returns a copy of input network
// where duplicate container endpoints in the network are removed.
// See https://github.com/docker/swarm/issues/1969
// This function should be disabled when the bug is fixed in Docker network
func (network *Network) RemoveDuplicateEndpoints() *Network {
// build a map from endpointID -> endpointIndex
endpointMap := make(map[string]string)
// traverse the endpoints to find the correct endpointIndex for each endpointID
for endpointIndex, endpointResource := range network.NetworkResource.Containers {
endpointID := endpointResource.EndpointID
// if this endpointID doesn't exist yet, add it
// if this endpointID exists, but endpointIndex is not a duplicate, use
// this endpointIndex
if _, ok := endpointMap[endpointID]; !ok || !strings.Contains(endpointIndex, endpointID) {
endpointMap[endpointID] = endpointIndex
}
}
// Make a copy of the network
netCopy := *network
// clean up existing endpoints
netCopy.Containers = make(map[string]types.EndpointResource)
// add the endpoint index from endpointMap
for _, index := range endpointMap {
netCopy.Containers[index] = network.Containers[index]
}
return &netCopy
}
// Get returns a network using its ID or Name
func (networks Networks) Get(IDOrName string) *Network {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
// Match exact or short Network ID.
for _, network := range networks {
if network.ID == IDOrName || stringid.TruncateID(network.ID) == IDOrName {
return network
}
}
candidates := Networks{}
// Match name or engine/name.
for _, network := range networks {
if network.Name == IDOrName || network.Engine.ID+"/"+network.Name == IDOrName || network.Engine.Name+"/"+network.Name == IDOrName {
candidates = append(candidates, network)
}
}
// Return if we found a unique match.
if size := len(candidates.Uniq()); size == 1 {
return candidates[0]
} else if size > 1 {
return nil
}
// Match /name and return as soon as we find one.
for _, network := range networks {
if network.Name == "/"+IDOrName {
return network
}
}
// Match Network ID prefix.
for _, network := range networks {
if strings.HasPrefix(network.ID, IDOrName) {
candidates = append(candidates, network)
}
}
if len(candidates.Uniq()) == 1 {
return candidates[0]
}
return nil
}

191
cluster/network_test.go Normal file
View File

@@ -0,0 +1,191 @@
package cluster
import (
"testing"
"github.com/docker/engine-api/types"
"github.com/stretchr/testify/assert"
)
func TestNetworksFilter(t *testing.T) {
engine := &Engine{ID: "id"}
networks := Networks{
{types.NetworkResource{
ID: "ababababab",
Name: "something",
}, engine},
{types.NetworkResource{
ID: "aaaaaaaaaa1",
Name: "network_name",
}, engine},
{types.NetworkResource{
ID: "bbbbbbbbbb",
Name: "somethingelse",
}, engine},
{types.NetworkResource{
ID: "aaaaaaaaa2",
Name: "foo",
}, engine},
}
filtered := networks.Filter([]string{"network_name"}, []string{"abababab"}, nil)
assert.Equal(t, len(filtered), 2)
for _, network := range filtered {
assert.True(t, network.ID == "aaaaaaaaaa1" || network.ID == "ababababab")
}
}
func TestNetworkUniq(t *testing.T) {
engine1 := &Engine{ID: "id1"}
engine2 := &Engine{ID: "id2"}
networks := Networks{
{types.NetworkResource{
ID: "global",
Name: "global",
Containers: map[string]types.EndpointResource{
"c1": {},
},
}, engine1},
{types.NetworkResource{
ID: "global",
Name: "global",
Containers: map[string]types.EndpointResource{
"c2": {},
},
}, engine2},
{types.NetworkResource{
ID: "local1",
Name: "local",
Containers: map[string]types.EndpointResource{
"c3": {},
},
}, engine1},
{types.NetworkResource{
ID: "local2",
Name: "local",
Containers: map[string]types.EndpointResource{
"c4": {},
},
}, engine2},
}
global := networks.Uniq().Get("global")
assert.NotNil(t, global)
assert.Equal(t, 2, len(global.Containers))
local1 := networks.Uniq().Get("local1")
assert.NotNil(t, local1)
assert.Equal(t, 1, len(local1.Containers))
local3 := networks.Uniq().Get("local3")
assert.Nil(t, local3)
}
func TestRemoveDuplicateEndpoints(t *testing.T) {
engine1 := &Engine{ID: "id1"}
network := Network{
types.NetworkResource{
ID: "global",
Name: "voteappbase_voteapp",
Containers: map[string]types.EndpointResource{
"028771f7f6a54c486d441ecfc92aad68e0836a1f0a5a0c227c514f14848e2b54": {
Name: "voteappbase_worker_1",
EndpointID: "49f621862a0659f462870a6cd15874da44592e399f41da2a3019d81b7427315b",
MacAddress: "02:42:0a:00:02:04",
IPv4Address: "10.0.2.4/24",
IPv6Address: "",
},
"6baa9c82be8fe731556b371312989e22fb9e67121698c94b4890dd554381d97b": {
Name: "voteappbase_voting-app_1",
EndpointID: "95e831e91a76c87a51b93217645fcaf093c209e2b6691493bfdc0cf2c39698d0",
MacAddress: "02:42:0a:00:02:05",
IPv4Address: "10.0.2.5/24",
IPv6Address: "",
},
"995eccf14e33797b15c2c1ba67f68b7ced0754c42f096e02cc1c488a418f6126": {
Name: "db",
EndpointID: "e881cf9ba6dcc5495630d4e2ffba9178f7f4456f12938624562e2cf79888e6b4",
MacAddress: "02:42:0a:00:02:02",
IPv4Address: "10.0.2.2/24",
IPv6Address: "",
},
"e4547f30d8dcd93a9c0da8b44e2a8f793eb01260aa478243605d137a90688611": {
Name: "voteappbase_result-app_1",
EndpointID: "d79ed2653731eccc968bd32da158bf74646511242d7a96f31fd350eda5b658cb",
MacAddress: "02:42:0a:00:02:06",
IPv4Address: "10.0.2.6/24",
IPv6Address: "",
},
"ep-49f621862a0659f462870a6cd15874da44592e399f41da2a3019d81b7427315b": {
Name: "voteappbase_worker_1",
EndpointID: "49f621862a0659f462870a6cd15874da44592e399f41da2a3019d81b7427315b",
MacAddress: "02:42:0a:00:02:04",
IPv4Address: "10.0.2.4/24",
IPv6Address: "",
},
"ep-95e831e91a76c87a51b93217645fcaf093c209e2b6691493bfdc0cf2c39698d0": {
Name: "voteappbase_voting-app_1",
EndpointID: "95e831e91a76c87a51b93217645fcaf093c209e2b6691493bfdc0cf2c39698d0",
MacAddress: "02:42:0a:00:02:05",
IPv4Address: "10.0.2.5/24",
IPv6Address: "",
},
"ep-d79ed2653731eccc968bd32da158bf74646511242d7a96f31fd350eda5b658cb": {
Name: "voteappbase_result-app_1",
EndpointID: "d79ed2653731eccc968bd32da158bf74646511242d7a96f31fd350eda5b658cb",
MacAddress: "02:42:0a:00:02:06",
IPv4Address: "10.0.2.6/24",
IPv6Address: "",
},
"ep-e881cf9ba6dcc5495630d4e2ffba9178f7f4456f12938624562e2cf79888e6b4": {
Name: "db",
EndpointID: "e881cf9ba6dcc5495630d4e2ffba9178f7f4456f12938624562e2cf79888e6b4",
MacAddress: "02:42:0a:00:02:02",
IPv4Address: "10.0.2.2/24",
IPv6Address: "",
},
"ep-eaa1cf9ba6dcc5495630d4e2ffba917af7f4456f1293a624562e2cf79aaae6b4": {
Name: "db-stale",
EndpointID: "eaa1cf9ba6dcc5495630d4e2ffba917af7f4456f1293a624562e2cf79aaae6b4",
MacAddress: "02:42:0a:00:02:32",
IPv4Address: "10.0.2.33/24",
IPv6Address: "",
},
"ep-f2e8540123a5b5894da462b8fd06de6394cb1a263392167b87e1b7195ec33055": {
Name: "redis",
EndpointID: "f2e8540123a5b5894da462b8fd06de6394cb1a263392167b87e1b7195ec33055",
MacAddress: "02:42:0a:00:02:03",
IPv4Address: "10.0.2.3/24",
IPv6Address: "",
},
"f7e42305c1c50b145814d28508312e7374edbafebd8f04115606e58fde96f441": {
Name: "redis",
EndpointID: "f2e8540123a5b5894da462b8fd06de6394cb1a263392167b87e1b7195ec33055",
MacAddress: "02:42:0a:00:02:03",
IPv4Address: "10.0.2.3/24",
IPv6Address: "",
},
},
}, engine1}
cleanNet := network.RemoveDuplicateEndpoints()
assert.Equal(t, len(cleanNet.Containers), 6)
// good endpoints are preserved
resource, ok := cleanNet.Containers["028771f7f6a54c486d441ecfc92aad68e0836a1f0a5a0c227c514f14848e2b54"]
assert.True(t, ok)
assert.Equal(t, resource.EndpointID, "49f621862a0659f462870a6cd15874da44592e399f41da2a3019d81b7427315b")
resource, ok = cleanNet.Containers["f7e42305c1c50b145814d28508312e7374edbafebd8f04115606e58fde96f441"]
assert.True(t, ok)
assert.Equal(t, resource.EndpointID, "f2e8540123a5b5894da462b8fd06de6394cb1a263392167b87e1b7195ec33055")
// duplicate endpoint should be removed
resource, ok = cleanNet.Containers["ep-f2e8540123a5b5894da462b8fd06de6394cb1a263392167b87e1b7195ec33055"]
assert.False(t, ok)
// stale endpoint is preserved
resource, ok = cleanNet.Containers["ep-eaa1cf9ba6dcc5495630d4e2ffba917af7f4456f1293a624562e2cf79aaae6b4"]
assert.True(t, ok)
assert.Equal(t, resource.EndpointID, "eaa1cf9ba6dcc5495630d4e2ffba917af7f4456f1293a624562e2cf79aaae6b4")
}

69
cluster/options.go Normal file
View File

@@ -0,0 +1,69 @@
package cluster
import (
"net"
"os"
"strconv"
"strings"
)
// DriverOpts are key=values options
type DriverOpts []string
// String returns a string from the driver options
func (do DriverOpts) String(key, env string) (string, bool) {
for _, opt := range do {
kv := strings.SplitN(opt, "=", 2)
if kv[0] == key {
return kv[1], true
}
}
if env := os.Getenv(env); env != "" {
return env, true
}
return "", false
}
// Int returns an int64 from the driver options
func (do DriverOpts) Int(key, env string) (int64, bool) {
if value, ok := do.String(key, env); ok {
v, _ := strconv.ParseInt(value, 0, 64)
return v, true
}
return 0, false
}
// Uint returns an int64 from the driver options
func (do DriverOpts) Uint(key, env string) (uint64, bool) {
if value, ok := do.String(key, env); ok {
v, _ := strconv.ParseUint(value, 0, 64)
return v, true
}
return 0, false
}
// Float returns a float64 from the driver options
func (do DriverOpts) Float(key, env string) (float64, bool) {
if value, ok := do.String(key, env); ok {
v, _ := strconv.ParseFloat(value, 64)
return v, true
}
return 0.0, false
}
// IP returns an IP address from the driver options
func (do DriverOpts) IP(key, env string) (net.IP, bool) {
if value, ok := do.String(key, env); ok {
return net.ParseIP(value), true
}
return nil, false
}
// Bool returns a boolean from the driver options
func (do DriverOpts) Bool(key, env string) (bool, bool) {
if value, ok := do.String(key, env); ok {
b, _ := strconv.ParseBool(value)
return b, true
}
return false, false
}

154
cluster/options_test.go Normal file
View File

@@ -0,0 +1,154 @@
package cluster
import (
"net"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
var opts = DriverOpts{"foo1=bar", "foo2=-5", "foo3=7", "foo4=0.6", "foo5=127.0.0.1"}
func TestString(t *testing.T) {
if err := os.Setenv("FOO_4", "bar"); err != nil {
t.Fatal(err)
}
defer os.Unsetenv("FOO_4")
val, ok := opts.String("foo1", "")
assert.True(t, ok)
assert.Equal(t, val, "bar")
val, ok = opts.String("foo2", "")
assert.True(t, ok)
assert.Equal(t, val, "-5")
val, ok = opts.String("foo3", "")
assert.True(t, ok)
assert.Equal(t, val, "7")
val, ok = opts.String("foo4", "")
assert.True(t, ok)
assert.Equal(t, val, "0.6")
val, ok = opts.String("", "FOO_4")
assert.True(t, ok)
assert.Equal(t, val, "bar")
val, ok = opts.String("invalid", "")
assert.False(t, ok)
assert.Equal(t, val, "")
}
func TestInt(t *testing.T) {
if err := os.Setenv("FOO_4", "bar"); err != nil {
t.Fatal(err)
}
defer os.Unsetenv("FOO_4")
val, ok := opts.Int("foo1", "")
assert.True(t, ok)
assert.Equal(t, val, int64(0))
val, ok = opts.Int("foo2", "")
assert.True(t, ok)
assert.Equal(t, val, int64(-5))
val, ok = opts.Int("foo3", "")
assert.True(t, ok)
assert.Equal(t, val, int64(7))
val, ok = opts.Int("foo4", "")
assert.True(t, ok)
assert.Equal(t, val, int64(0))
val, ok = opts.Int("", "FOO_4")
assert.True(t, ok)
assert.Equal(t, val, int64(0))
val, ok = opts.Int("invalid", "")
assert.False(t, ok)
assert.Equal(t, val, int64(0))
}
func TestUint(t *testing.T) {
if err := os.Setenv("FOO_4", "bar"); err != nil {
t.Fatal(err)
}
defer os.Unsetenv("FOO_4")
val, ok := opts.Uint("foo1", "")
assert.True(t, ok)
assert.Equal(t, val, uint64(0))
val, ok = opts.Uint("foo2", "")
assert.True(t, ok)
assert.Equal(t, val, uint64(0))
val, ok = opts.Uint("foo3", "")
assert.True(t, ok)
assert.Equal(t, val, uint64(7))
val, ok = opts.Uint("foo4", "")
assert.True(t, ok)
assert.Equal(t, val, uint64(0))
val, ok = opts.Uint("", "FOO_4")
assert.True(t, ok)
assert.Equal(t, val, uint64(0))
val, ok = opts.Uint("invalid", "")
assert.False(t, ok)
assert.Equal(t, val, uint64(0))
}
func TestFloat(t *testing.T) {
if err := os.Setenv("FOO_4", "0.2"); err != nil {
t.Fatal(err)
}
defer os.Unsetenv("FOO_4")
val, ok := opts.Float("foo1", "")
assert.True(t, ok)
assert.Equal(t, val, 0.0)
val, ok = opts.Float("foo2", "")
assert.True(t, ok)
assert.Equal(t, val, -5.0)
val, ok = opts.Float("foo3", "")
assert.True(t, ok)
assert.Equal(t, val, 7.0)
val, ok = opts.Float("foo4", "")
assert.True(t, ok)
assert.Equal(t, val, 0.6)
val, ok = opts.Float("", "FOO_4")
assert.True(t, ok)
assert.Equal(t, val, 0.2)
val, ok = opts.Float("invalid", "")
assert.False(t, ok)
assert.Equal(t, val, 0.0)
}
func TestIP(t *testing.T) {
if err := os.Setenv("FOO_4", "0.0.0.0"); err != nil {
t.Fatal(err)
}
defer os.Unsetenv("FOO_4")
val, ok := opts.IP("foo5", "")
assert.True(t, ok)
assert.Equal(t, val, net.ParseIP("127.0.0.1"))
val, ok = opts.IP("", "FOO_4")
assert.True(t, ok)
assert.Equal(t, val, net.ParseIP("0.0.0.0"))
val, ok = opts.IP("invalid", "")
assert.False(t, ok)
assert.Equal(t, val, net.IP(nil))
}

995
cluster/swarm/cluster.go Normal file
View File

@@ -0,0 +1,995 @@
package swarm
import (
"crypto/tls"
"errors"
"fmt"
"io"
"regexp"
"sort"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/discovery"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/engine-api/client"
engineapi "github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
"github.com/docker/go-units"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler"
"github.com/docker/swarm/scheduler/node"
"github.com/samalba/dockerclient"
)
type pendingContainer struct {
Config *cluster.ContainerConfig
Name string
Engine *cluster.Engine
}
func (p *pendingContainer) ToContainer() *cluster.Container {
container := &cluster.Container{
Container: types.Container{
Labels: p.Config.Labels,
},
Config: p.Config,
Info: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &containertypes.HostConfig{},
},
},
Engine: p.Engine,
}
if p.Name != "" {
container.Container.Names = []string{"/" + p.Name}
}
return container
}
// Cluster is exported
type Cluster struct {
sync.RWMutex
eventHandlers *cluster.EventHandlers
engines map[string]*cluster.Engine
pendingEngines map[string]*cluster.Engine
scheduler *scheduler.Scheduler
discovery discovery.Backend
pendingContainers map[string]*pendingContainer
overcommitRatio float64
engineOpts *cluster.EngineOpts
createRetry int64
TLSConfig *tls.Config
}
// NewCluster is exported
func NewCluster(scheduler *scheduler.Scheduler, TLSConfig *tls.Config, discovery discovery.Backend, options cluster.DriverOpts, engineOptions *cluster.EngineOpts) (cluster.Cluster, error) {
log.WithFields(log.Fields{"name": "swarm"}).Debug("Initializing cluster")
cluster := &Cluster{
eventHandlers: cluster.NewEventHandlers(),
engines: make(map[string]*cluster.Engine),
pendingEngines: make(map[string]*cluster.Engine),
scheduler: scheduler,
TLSConfig: TLSConfig,
discovery: discovery,
pendingContainers: make(map[string]*pendingContainer),
overcommitRatio: 0.05,
engineOpts: engineOptions,
createRetry: 0,
}
if val, ok := options.Float("swarm.overcommit", ""); ok {
if val <= float64(-1) {
log.Fatalf("swarm.overcommit should be larger than -1, %f is invalid", val)
} else if val < float64(0) {
log.Warn("-1 < swarm.overcommit < 0 will make swarm take less resource than docker engine offers")
cluster.overcommitRatio = val
} else {
cluster.overcommitRatio = val
}
}
if val, ok := options.Int("swarm.createretry", ""); ok {
if val < 0 {
log.Fatalf("swarm.createretry can not be negative, %d is invalid", val)
}
cluster.createRetry = val
}
discoveryCh, errCh := cluster.discovery.Watch(nil)
go cluster.monitorDiscovery(discoveryCh, errCh)
go cluster.monitorPendingEngines()
return cluster, nil
}
// Handle callbacks for the events
func (c *Cluster) Handle(e *cluster.Event) error {
c.eventHandlers.Handle(e)
return nil
}
// RegisterEventHandler registers an event handler.
func (c *Cluster) RegisterEventHandler(h cluster.EventHandler) error {
return c.eventHandlers.RegisterEventHandler(h)
}
// UnregisterEventHandler unregisters a previously registered event handler.
func (c *Cluster) UnregisterEventHandler(h cluster.EventHandler) {
c.eventHandlers.UnregisterEventHandler(h)
}
// Generate a globally (across the cluster) unique ID.
func (c *Cluster) generateUniqueID() string {
for {
id := stringid.GenerateRandomID()
if c.Container(id) == nil {
return id
}
}
}
// StartContainer starts a container
func (c *Cluster) StartContainer(container *cluster.Container, hostConfig *dockerclient.HostConfig) error {
return container.Engine.StartContainer(container.ID, hostConfig)
}
// CreateContainer aka schedule a brand new container into the cluster.
func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string, authConfig *types.AuthConfig) (*cluster.Container, error) {
container, err := c.createContainer(config, name, false, authConfig)
if err != nil {
var retries int64
// fails with image not found, then try to reschedule with image affinity
// ENGINEAPIFIXME: The first error can be removed once dockerclient is removed
bImageNotFoundError, _ := regexp.MatchString(`image \S* not found`, err.Error())
if (bImageNotFoundError || client.IsErrImageNotFound(err)) && !config.HaveNodeConstraint() {
// Check if the image exists in the cluster
// If exists, retry with an image affinity
if c.Image(config.Image) != nil {
container, err = c.createContainer(config, name, true, authConfig)
retries++
}
}
for ; retries < c.createRetry && err != nil; retries++ {
log.WithFields(log.Fields{"Name": "Swarm"}).Warnf("Failed to create container: %s, retrying", err)
container, err = c.createContainer(config, name, false, authConfig)
}
}
return container, err
}
func (c *Cluster) createContainer(config *cluster.ContainerConfig, name string, withImageAffinity bool, authConfig *types.AuthConfig) (*cluster.Container, error) {
c.scheduler.Lock()
// Ensure the name is available
if !c.checkNameUniqueness(name) {
c.scheduler.Unlock()
return nil, fmt.Errorf("Conflict: The name %s is already assigned. You have to delete (or rename) that container to be able to assign %s to a container again.", name, name)
}
swarmID := config.SwarmID()
if swarmID == "" {
// Associate a Swarm ID to the container we are creating.
swarmID = c.generateUniqueID()
config.SetSwarmID(swarmID)
}
if network := c.Networks().Get(string(config.HostConfig.NetworkMode)); network != nil && network.Scope == "local" {
if !config.HaveNodeConstraint() {
config.AddConstraint("node==~" + network.Engine.Name)
}
config.HostConfig.NetworkMode = containertypes.NetworkMode(network.Name)
}
if withImageAffinity {
config.AddAffinity("image==" + config.Image)
}
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
if withImageAffinity {
config.RemoveAffinity("image==" + config.Image)
}
if err != nil {
c.scheduler.Unlock()
return nil, err
}
n := nodes[0]
engine, ok := c.engines[n.ID]
if !ok {
c.scheduler.Unlock()
return nil, fmt.Errorf("error creating container")
}
c.pendingContainers[swarmID] = &pendingContainer{
Name: name,
Config: config,
Engine: engine,
}
c.scheduler.Unlock()
container, err := engine.CreateContainer(config, name, true, authConfig)
if err != nil {
log.WithFields(log.Fields{"NodeName": n.Name, "NodeID": n.ID}).WithError(err).Error("Failed to create container")
} else {
containerFlag := name
if containerFlag == "" {
containerFlag = stringid.TruncateID(container.ID)
}
log.WithFields(log.Fields{"NodeName": n.Name, "NodeID": n.ID}).Debugf("Scheduling container %s to ", containerFlag)
}
c.scheduler.Lock()
delete(c.pendingContainers, swarmID)
c.scheduler.Unlock()
return container, err
}
// RemoveContainer aka Remove a container from the cluster.
func (c *Cluster) RemoveContainer(container *cluster.Container, force, volumes bool) error {
return container.Engine.RemoveContainer(container, force, volumes)
}
// RemoveNetwork removes a network from the cluster
func (c *Cluster) RemoveNetwork(network *cluster.Network) error {
err := network.Engine.RemoveNetwork(network)
if err == nil {
if network.Scope == "global" {
for _, engine := range c.engines {
engine.DeleteNetwork(network)
}
}
} else if engineapi.ErrConnectionFailed == err && network.Scope == "global" {
log.Debug("The original engine is unreachable - Attempting to remove global network from the reachable engines...")
for _, engine := range c.engines {
e1 := engine.RemoveNetwork(network)
if e1 == nil {
for _, engine := range c.engines {
engine.DeleteNetwork(network)
}
err = nil
break
}
}
}
if err != nil && network.Scope == "global" {
log.Debugf("Failed to remove global scope network %s from any engine...", network.ID)
}
return err
}
func (c *Cluster) getEngineByAddr(addr string) *cluster.Engine {
c.RLock()
defer c.RUnlock()
if engine, ok := c.pendingEngines[addr]; ok {
return engine
}
for _, engine := range c.engines {
if engine.Addr == addr {
return engine
}
}
return nil
}
func (c *Cluster) hasEngineByAddr(addr string) bool {
return c.getEngineByAddr(addr) != nil
}
func (c *Cluster) addEngine(addr string) bool {
// Check the engine is already registered by address.
if c.hasEngineByAddr(addr) {
return false
}
engine := cluster.NewEngine(addr, c.overcommitRatio, c.engineOpts)
if err := engine.RegisterEventHandler(c); err != nil {
log.Error(err)
}
// Add it to pending engine map, indexed by address. This will prevent
// duplicates from entering
c.Lock()
c.pendingEngines[addr] = engine
c.Unlock()
// validatePendingEngine will start a thread to validate the engine.
// If the engine is reachable and valid, it'll be monitored and updated in a loop.
// If engine is not reachable, pending engines will be examined once in a while
go c.validatePendingEngine(engine)
return true
}
// validatePendingEngine connects to the engine,
func (c *Cluster) validatePendingEngine(engine *cluster.Engine) bool {
// Attempt a connection to the engine. Since this is slow, don't get a hold
// of the lock yet.
if err := engine.Connect(c.TLSConfig); err != nil {
log.WithFields(log.Fields{"Addr": engine.Addr}).Debugf("Failed to validate pending node: %s", err)
return false
}
// The following is critical and fast. Grab a lock.
c.Lock()
defer c.Unlock()
// Only validate engines from pendingEngines list
if _, exists := c.pendingEngines[engine.Addr]; !exists {
return false
}
// Make sure the engine ID is unique.
if old, exists := c.engines[engine.ID]; exists {
if old.Addr != engine.Addr {
log.Errorf("ID duplicated. %s shared by %s and %s", engine.ID, old.Addr, engine.Addr)
// Keep this engine in pendingEngines table and show its error.
// If it's ID duplication from VM clone, user see this message and can fix it.
// If the engine is rebooted and get new IP from DHCP, previous address will be removed
// from discovery after a while.
// In both cases, retry may fix the problem.
engine.HandleIDConflict(old.Addr)
} else {
log.Debugf("node %q (name: %q) with address %q is already registered", engine.ID, engine.Name, engine.Addr)
engine.Disconnect()
// Remove it from pendingEngines table
delete(c.pendingEngines, engine.Addr)
}
return false
}
// Engine validated, move from pendingEngines table to engines table
delete(c.pendingEngines, engine.Addr)
// set engine state to healthy, and start refresh loop
engine.ValidationComplete()
c.engines[engine.ID] = engine
log.Infof("Registered Engine %s at %s", engine.Name, engine.Addr)
return true
}
func (c *Cluster) removeEngine(addr string) bool {
engine := c.getEngineByAddr(addr)
if engine == nil {
return false
}
c.Lock()
defer c.Unlock()
engine.Disconnect()
// it could be in pendingEngines or engines
if _, ok := c.pendingEngines[addr]; ok {
delete(c.pendingEngines, addr)
} else {
delete(c.engines, engine.ID)
}
log.Infof("Removed Engine %s", engine.Name)
return true
}
// Entries are Docker Engines
func (c *Cluster) monitorDiscovery(ch <-chan discovery.Entries, errCh <-chan error) {
// Watch changes on the discovery channel.
currentEntries := discovery.Entries{}
for {
select {
case entries := <-ch:
added, removed := currentEntries.Diff(entries)
currentEntries = entries
// Remove engines first. `addEngine` will refuse to add an engine
// if there's already an engine with the same ID. If an engine
// changes address, we have to first remove it then add it back.
for _, entry := range removed {
c.removeEngine(entry.String())
}
for _, entry := range added {
c.addEngine(entry.String())
}
case err := <-errCh:
log.Errorf("Discovery error: %v", err)
}
}
}
// monitorPendingEngines checks if some previous unreachable/invalid engines have been fixed
func (c *Cluster) monitorPendingEngines() {
const minimumValidationInterval time.Duration = 10 * time.Second
for {
// Don't need to do it frequently
time.Sleep(minimumValidationInterval)
// Get the list of pendingEngines
c.RLock()
pEngines := make([]*cluster.Engine, 0, len(c.pendingEngines))
for _, e := range c.pendingEngines {
pEngines = append(pEngines, e)
}
c.RUnlock()
for _, e := range pEngines {
if e.TimeToValidate() {
go c.validatePendingEngine(e)
}
}
}
}
// Images returns all the images in the cluster.
func (c *Cluster) Images() cluster.Images {
c.RLock()
defer c.RUnlock()
out := []*cluster.Image{}
for _, e := range c.engines {
out = append(out, e.Images()...)
}
return out
}
// Image returns an image with IDOrName in the cluster
func (c *Cluster) Image(IDOrName string) *cluster.Image {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
c.RLock()
defer c.RUnlock()
for _, e := range c.engines {
if image := e.Image(IDOrName); image != nil {
return image
}
}
return nil
}
// RemoveImages removes all the images that match `name` from the cluster
func (c *Cluster) RemoveImages(name string, force bool) ([]types.ImageDelete, error) {
c.Lock()
defer c.Unlock()
out := []types.ImageDelete{}
errs := []string{}
var err error
for _, e := range c.engines {
for _, image := range e.Images() {
if image.Match(name, true) {
content, err := image.Engine.RemoveImage(name, force)
if err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", image.Engine.Name, err.Error()))
continue
}
out = append(out, content...)
}
}
}
if len(errs) > 0 {
err = errors.New(strings.Join(errs, "\n"))
}
return out, err
}
// CreateNetwork creates a network in the cluster
func (c *Cluster) CreateNetwork(name string, request *types.NetworkCreate) (response *types.NetworkCreateResponse, err error) {
var (
parts = strings.SplitN(name, "/", 2)
config = &cluster.ContainerConfig{}
)
if len(parts) == 2 {
// a node was specified, create the container only on this node
name = parts[1]
config = cluster.BuildContainerConfig(containertypes.Config{Env: []string{"constraint:node==" + parts[0]}}, containertypes.HostConfig{}, networktypes.NetworkingConfig{})
}
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
if err != nil {
return nil, err
}
if nodes != nil {
resp, err := c.engines[nodes[0].ID].CreateNetwork(name, request)
if err == nil {
if network := c.engines[nodes[0].ID].Networks().Get(resp.ID); network != nil && network.Scope == "global" {
for id, engine := range c.engines {
if id != nodes[0].ID {
engine.AddNetwork(network)
}
}
}
}
return resp, err
}
return nil, nil
}
// CreateVolume creates a volume in the cluster
func (c *Cluster) CreateVolume(request *types.VolumeCreateRequest) (*types.Volume, error) {
var (
wg sync.WaitGroup
volume *types.Volume
err error
parts = strings.SplitN(request.Name, "/", 2)
node = ""
)
if request.Name == "" {
request.Name = stringid.GenerateRandomID()
} else if len(parts) == 2 {
node = parts[0]
request.Name = parts[1]
}
if node == "" {
c.RLock()
for _, e := range c.engines {
wg.Add(1)
go func(engine *cluster.Engine) {
defer wg.Done()
v, er := engine.CreateVolume(request)
if v != nil {
volume = v
err = nil
}
if er != nil && volume == nil {
err = er
}
}(e)
}
c.RUnlock()
wg.Wait()
} else {
config := cluster.BuildContainerConfig(containertypes.Config{Env: []string{"constraint:node==" + parts[0]}}, containertypes.HostConfig{}, networktypes.NetworkingConfig{})
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
if err != nil {
return nil, err
}
if nodes != nil {
v, er := c.engines[nodes[0].ID].CreateVolume(request)
if v != nil {
volume = v
err = nil
}
if er != nil && volume == nil {
err = er
}
}
}
return volume, err
}
// RemoveVolumes removes all the volumes that match `name` from the cluster
func (c *Cluster) RemoveVolumes(name string) (bool, error) {
c.Lock()
defer c.Unlock()
found := false
errs := []string{}
var err error
for _, e := range c.engines {
if volume := e.Volumes().Get(name); volume != nil {
if err := volume.Engine.RemoveVolume(volume.Name); err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", volume.Engine.Name, err.Error()))
continue
}
found = true
}
}
if len(errs) > 0 {
err = errors.New(strings.Join(errs, "\n"))
}
return found, err
}
// Pull is exported
func (c *Cluster) Pull(name string, authConfig *types.AuthConfig, callback func(where, status string, err error)) {
var wg sync.WaitGroup
c.RLock()
for _, e := range c.engines {
wg.Add(1)
go func(engine *cluster.Engine) {
defer wg.Done()
if callback != nil {
callback(engine.Name, "", nil)
}
err := engine.Pull(name, authConfig)
if callback != nil {
if err != nil {
callback(engine.Name, "", err)
} else {
callback(engine.Name, "downloaded", nil)
}
}
}(e)
}
c.RUnlock()
wg.Wait()
}
// Load image
func (c *Cluster) Load(imageReader io.Reader, callback func(where, status string, err error)) {
var wg sync.WaitGroup
c.RLock()
pipeWriters := []*io.PipeWriter{}
for _, e := range c.engines {
wg.Add(1)
pipeReader, pipeWriter := io.Pipe()
pipeWriters = append(pipeWriters, pipeWriter)
go func(reader *io.PipeReader, engine *cluster.Engine) {
defer wg.Done()
defer reader.Close()
// call engine load image
err := engine.Load(reader)
if callback != nil {
if err != nil {
callback(engine.Name, "", err)
}
}
}(pipeReader, e)
}
c.RUnlock()
// create multi-writer
listWriter := []io.Writer{}
for _, pipeW := range pipeWriters {
listWriter = append(listWriter, pipeW)
}
multiWriter := io.MultiWriter(listWriter...)
// copy image-reader to multi-writer
_, err := io.Copy(multiWriter, imageReader)
if err != nil {
log.Error(err)
}
// close pipe writers
for _, pipeW := range pipeWriters {
pipeW.Close()
}
wg.Wait()
}
// Import image
func (c *Cluster) Import(source string, ref string, tag string, imageReader io.Reader, callback func(what, status string, err error)) {
var wg sync.WaitGroup
c.RLock()
pipeWriters := []*io.PipeWriter{}
for _, e := range c.engines {
wg.Add(1)
pipeReader, pipeWriter := io.Pipe()
pipeWriters = append(pipeWriters, pipeWriter)
go func(reader *io.PipeReader, engine *cluster.Engine) {
defer wg.Done()
defer reader.Close()
// call engine import
err := engine.Import(source, ref, tag, reader)
if callback != nil {
if err != nil {
callback(engine.Name, "", err)
} else {
callback(engine.Name, "Import success", nil)
}
}
}(pipeReader, e)
}
c.RUnlock()
// create multi-writer
listWriter := []io.Writer{}
for _, pipeW := range pipeWriters {
listWriter = append(listWriter, pipeW)
}
multiWriter := io.MultiWriter(listWriter...)
// copy image-reader to muti-writer
_, err := io.Copy(multiWriter, imageReader)
if err != nil {
log.Error(err)
}
// close pipe writers
for _, pipeW := range pipeWriters {
pipeW.Close()
}
wg.Wait()
}
// Containers returns all the containers in the cluster.
func (c *Cluster) Containers() cluster.Containers {
c.RLock()
defer c.RUnlock()
out := cluster.Containers{}
for _, e := range c.engines {
out = append(out, e.Containers()...)
}
return out
}
func (c *Cluster) checkNameUniqueness(name string) bool {
// Abort immediately if the name is empty.
if len(name) == 0 {
return true
}
c.RLock()
defer c.RUnlock()
for _, e := range c.engines {
for _, c := range e.Containers() {
for _, cname := range c.Names {
if cname == name || cname == "/"+name {
return false
}
}
}
}
// check pending containers.
for _, c := range c.pendingContainers {
if c.Name == name {
return false
}
}
return true
}
// Container returns the container with IDOrName in the cluster
func (c *Cluster) Container(IDOrName string) *cluster.Container {
// Abort immediately if the name is empty.
if len(IDOrName) == 0 {
return nil
}
return c.Containers().Get(IDOrName)
}
// Networks returns all the networks in the cluster.
func (c *Cluster) Networks() cluster.Networks {
c.RLock()
defer c.RUnlock()
out := cluster.Networks{}
for _, e := range c.engines {
out = append(out, e.Networks()...)
}
return out
}
// Volumes returns all the volumes in the cluster.
func (c *Cluster) Volumes() cluster.Volumes {
c.RLock()
defer c.RUnlock()
out := cluster.Volumes{}
for _, e := range c.engines {
out = append(out, e.Volumes()...)
}
return out
}
// listNodes returns all validated engines in the cluster, excluding pendingEngines.
func (c *Cluster) listNodes() []*node.Node {
c.RLock()
defer c.RUnlock()
out := make([]*node.Node, 0, len(c.engines))
for _, e := range c.engines {
node := node.NewNode(e)
for _, pc := range c.pendingContainers {
if pc.Engine.ID == e.ID && node.Container(pc.Config.SwarmID()) == nil {
node.AddContainer(pc.ToContainer())
}
}
out = append(out, node)
}
return out
}
// listEngines returns all the engines in the cluster.
// This is for reporting, not scheduling, hence pendingEngines are included.
func (c *Cluster) listEngines() []*cluster.Engine {
c.RLock()
defer c.RUnlock()
out := make([]*cluster.Engine, 0, len(c.engines)+len(c.pendingEngines))
for _, n := range c.engines {
out = append(out, n)
}
for _, n := range c.pendingEngines {
out = append(out, n)
}
return out
}
// TotalMemory returns the total memory of the cluster
func (c *Cluster) TotalMemory() int64 {
var totalMemory int64
for _, engine := range c.engines {
totalMemory += engine.TotalMemory()
}
return totalMemory
}
// TotalCpus returns the total CPUs of the cluster
func (c *Cluster) TotalCpus() int64 {
var totalCpus int64
for _, engine := range c.engines {
totalCpus += engine.TotalCpus()
}
return totalCpus
}
// Info returns some info about the cluster, like nb or containers / images
func (c *Cluster) Info() [][2]string {
info := [][2]string{
{"Strategy", c.scheduler.Strategy()},
{"Filters", c.scheduler.Filters()},
{"Nodes", fmt.Sprintf("%d", len(c.engines)+len(c.pendingEngines))},
}
engines := c.listEngines()
sort.Sort(cluster.EngineSorter(engines))
for _, engine := range engines {
engineName := "(unknown)"
if engine.Name != "" {
engineName = engine.Name
}
info = append(info, [2]string{" " + engineName, engine.Addr})
info = append(info, [2]string{" └ ID", engine.ID})
info = append(info, [2]string{" └ Status", engine.Status()})
// if engine's status is healthy, show container details of the node
if engine.IsHealthy() {
var paused, running, stopped int = 0, 0, 0
for _, c := range engine.Containers() {
if c.Info.State.Paused {
paused++
} else if c.Info.State.Running {
running++
} else {
stopped++
}
}
info = append(info, [2]string{" └ Containers", fmt.Sprintf("%d (%d Running, %d Paused, %d Stopped)", len(engine.Containers()), running, paused, stopped)})
} else {
info = append(info, [2]string{" └ Containers", fmt.Sprintf("%d", len(engine.Containers()))})
}
info = append(info, [2]string{" └ Reserved CPUs", fmt.Sprintf("%d / %d", engine.UsedCpus(), engine.TotalCpus())})
info = append(info, [2]string{" └ Reserved Memory", fmt.Sprintf("%s / %s", units.BytesSize(float64(engine.UsedMemory())), units.BytesSize(float64(engine.TotalMemory())))})
labels := make([]string, 0, len(engine.Labels))
for k, v := range engine.Labels {
labels = append(labels, k+"="+v)
}
sort.Strings(labels)
info = append(info, [2]string{" └ Labels", fmt.Sprintf("%s", strings.Join(labels, ", "))})
errMsg := engine.ErrMsg()
if len(errMsg) != 0 {
info = append(info, [2]string{" └ Error", errMsg})
}
info = append(info, [2]string{" └ UpdatedAt", engine.UpdatedAt().UTC().Format(time.RFC3339)})
info = append(info, [2]string{" └ ServerVersion", engine.Version})
}
return info
}
// RANDOMENGINE returns a random engine.
func (c *Cluster) RANDOMENGINE() (*cluster.Engine, error) {
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), &cluster.ContainerConfig{})
if err != nil {
return nil, err
}
return c.engines[nodes[0].ID], nil
}
// RenameContainer renames a container
func (c *Cluster) RenameContainer(container *cluster.Container, newName string) error {
// check new name whether available
if !c.checkNameUniqueness(newName) {
return fmt.Errorf("Conflict: The name %s is already assigned. You have to delete (or rename) that container to be able to assign %s to a container again.", newName, newName)
}
// call engine rename
err := container.Engine.RenameContainer(container, newName)
return err
}
// BuildImage builds an image
func (c *Cluster) BuildImage(buildContext io.Reader, buildImage *types.ImageBuildOptions, out io.Writer) error {
c.scheduler.Lock()
// get an engine
config := cluster.BuildContainerConfig(containertypes.Config{Env: convertMapToKVStrings(buildImage.BuildArgs)},
containertypes.HostConfig{Resources: containertypes.Resources{CPUShares: buildImage.CPUShares, Memory: buildImage.Memory}},
networktypes.NetworkingConfig{})
buildImage.BuildArgs = convertKVStringsToMap(config.Env)
nodes, err := c.scheduler.SelectNodesForContainer(c.listNodes(), config)
c.scheduler.Unlock()
if err != nil {
return err
}
n := nodes[0]
reader, err := c.engines[n.ID].BuildImage(buildContext, buildImage)
if err != nil {
return err
}
if _, err := io.Copy(out, reader); err != nil {
return err
}
c.engines[n.ID].RefreshImages()
return nil
}
// TagImage tags an image
func (c *Cluster) TagImage(IDOrName string, ref string, force bool) error {
c.RLock()
defer c.RUnlock()
errs := []string{}
var err error
found := false
for _, e := range c.engines {
for _, image := range e.Images() {
if image.Match(IDOrName, true) {
found = true
err := image.Engine.TagImage(IDOrName, ref, force)
if err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", image.Engine.Name, err.Error()))
continue
}
}
}
}
if !found {
return fmt.Errorf("No such image: %s", IDOrName)
}
if len(errs) > 0 {
err = errors.New(strings.Join(errs, "\n"))
}
return err
}

View File

@@ -0,0 +1,270 @@
package swarm
import (
"bytes"
"fmt"
"io"
"testing"
"time"
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
engineapimock "github.com/docker/swarm/api/mockclient"
"github.com/docker/swarm/cluster"
"github.com/samalba/dockerclient/mockclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type nopCloser struct {
io.Reader
}
// Close
func (nopCloser) Close() error {
return nil
}
var (
mockInfo = types.Info{
ID: "test-engine",
Name: "name",
NCPU: 10,
MemTotal: 20,
Driver: "driver-test",
ExecutionDriver: "execution-driver-test",
KernelVersion: "1.2.3",
OperatingSystem: "golang",
Labels: []string{"foo=bar"},
}
mockVersion = types.Version{
Version: "1.8.2",
}
engOpts = &cluster.EngineOpts{
RefreshMinInterval: time.Duration(30) * time.Second,
RefreshMaxInterval: time.Duration(60) * time.Second,
FailureRetry: 3,
}
)
// FIXMEENGINEAPI : Need to write more unit tests for creating/inspecting containers with engine-api
func createEngine(t *testing.T, ID string, containers ...*cluster.Container) *cluster.Engine {
engine := cluster.NewEngine(ID, 0, engOpts)
engine.Name = ID
engine.ID = ID
for _, container := range containers {
container.Engine = engine
engine.AddContainer(container)
}
return engine
}
func TestContainerLookup(t *testing.T) {
c := &Cluster{
engines: make(map[string]*cluster.Engine),
}
container1 := &cluster.Container{
Container: types.Container{
ID: "container1-id",
Names: []string{"/container1-name1", "/container1-name2"},
},
Config: cluster.BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.id": "swarm1-id",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}
container2 := &cluster.Container{
Container: types.Container{
ID: "container2-id",
Names: []string{"/con"},
},
Config: cluster.BuildContainerConfig(containertypes.Config{
Labels: map[string]string{
"com.docker.swarm.id": "swarm2-id",
},
}, containertypes.HostConfig{}, networktypes.NetworkingConfig{}),
}
n := createEngine(t, "test-engine", container1, container2)
c.engines[n.ID] = n
assert.Equal(t, len(c.Containers()), 2)
// Invalid lookup
assert.Nil(t, c.Container("invalid-id"))
assert.Nil(t, c.Container(""))
// Container ID lookup.
assert.NotNil(t, c.Container("container1-id"))
// Container ID prefix lookup.
assert.NotNil(t, c.Container("container1-"))
assert.Nil(t, c.Container("container"))
// Container name lookup.
assert.NotNil(t, c.Container("container1-name1"))
assert.NotNil(t, c.Container("container1-name2"))
// Container engine/name matching.
assert.NotNil(t, c.Container("test-engine/container1-name1"))
assert.NotNil(t, c.Container("test-engine/container1-name2"))
// Swarm ID lookup.
assert.NotNil(t, c.Container("swarm1-id"))
// Swarm ID prefix lookup.
assert.NotNil(t, c.Container("swarm1-"))
assert.Nil(t, c.Container("swarm"))
// Match name before ID prefix
cc := c.Container("con")
assert.NotNil(t, cc)
assert.Equal(t, cc.ID, "container2-id")
}
func TestImportImage(t *testing.T) {
// create cluster
c := &Cluster{
engines: make(map[string]*cluster.Engine),
}
// create engione
id := "test-engine"
engine := cluster.NewEngine(id, 0, engOpts)
engine.Name = id
engine.ID = id
// create mock client
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything, mock.Anything).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{bytes.NewBufferString("")}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil).Once()
// connect client
engine.ConnectWithClient(client, apiClient)
// add engine to cluster
c.engines[engine.ID] = engine
// import success
readCloser := nopCloser{bytes.NewBufferString("ok")}
apiClient.On("ImageImport", mock.Anything, mock.AnythingOfType("types.ImageImportSource"), mock.Anything, mock.AnythingOfType("types.ImageImportOptions")).Return(readCloser, nil).Once()
callback := func(what, status string, err error) {
// import success
assert.Nil(t, err)
}
c.Import("-", "testImageOK", "latest", bytes.NewReader(nil), callback)
// import error
readCloser = nopCloser{bytes.NewBufferString("error")}
err := fmt.Errorf("Import error")
apiClient.On("ImageImport", mock.Anything, mock.AnythingOfType("types.ImageImportSource"), mock.Anything, mock.AnythingOfType("types.ImageImportOptions")).Return(readCloser, err).Once()
callback = func(what, status string, err error) {
// import error
assert.NotNil(t, err)
}
c.Import("-", "testImageError", "latest", bytes.NewReader(nil), callback)
}
func TestLoadImage(t *testing.T) {
// create cluster
c := &Cluster{
engines: make(map[string]*cluster.Engine),
}
// create engione
id := "test-engine"
engine := cluster.NewEngine(id, 0, engOpts)
engine.Name = id
engine.ID = id
// create mock client
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything, mock.Anything).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{bytes.NewBufferString("")}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return([]types.Image{}, nil)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil).Once()
// connect client
engine.ConnectWithClient(client, apiClient)
// add engine to cluster
c.engines[engine.ID] = engine
// load success
readCloser := nopCloser{bytes.NewBufferString("")}
apiClient.On("ImageLoad", mock.Anything, mock.AnythingOfType("*io.PipeReader"), false).Return(types.ImageLoadResponse{Body: readCloser}, nil).Once()
callback := func(what, status string, err error) {
//if load OK, err will be nil
assert.Nil(t, err)
}
c.Load(bytes.NewReader(nil), callback)
// load error
err := fmt.Errorf("Load error")
apiClient.On("ImageLoad", mock.Anything, mock.AnythingOfType("*io.PipeReader"), false).Return(types.ImageLoadResponse{}, err).Once()
callback = func(what, status string, err error) {
// load error, err is not nil
assert.NotNil(t, err)
}
c.Load(bytes.NewReader(nil), callback)
}
func TestTagImage(t *testing.T) {
// create cluster
c := &Cluster{
engines: make(map[string]*cluster.Engine),
}
images := []types.Image{}
image1 := types.Image{
ID: "1234567890",
RepoTags: []string{"busybox:latest"},
}
images = append(images, image1)
// create engine
id := "test-engine"
engine := cluster.NewEngine(id, 0, engOpts)
engine.Name = id
engine.ID = id
// create mock client
client := mockclient.NewMockClient()
apiClient := engineapimock.NewMockClient()
apiClient.On("Info", mock.Anything).Return(mockInfo, nil)
apiClient.On("ServerVersion", mock.Anything).Return(mockVersion, nil)
apiClient.On("NetworkList", mock.Anything,
mock.AnythingOfType("NetworkListOptions"),
).Return([]types.NetworkResource{}, nil)
apiClient.On("VolumeList", mock.Anything, mock.Anything).Return(types.VolumesListResponse{}, nil)
apiClient.On("Events", mock.Anything, mock.AnythingOfType("EventsOptions")).Return(&nopCloser{bytes.NewBufferString("")}, nil)
apiClient.On("ImageList", mock.Anything, mock.AnythingOfType("ImageListOptions")).Return(images, nil)
apiClient.On("ContainerList", mock.Anything, types.ContainerListOptions{All: true, Size: false}).Return([]types.Container{}, nil).Once()
// connect client
engine.ConnectWithClient(client, apiClient)
// add engine to cluster
c.engines[engine.ID] = engine
// tag image
apiClient.On("ImageTag", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
assert.Nil(t, c.TagImage("busybox", "test_busybox:latest", false))
assert.NotNil(t, c.TagImage("busybox_not_exists", "test_busybox:latest", false))
}

31
cluster/swarm/utils.go Normal file
View File

@@ -0,0 +1,31 @@
package swarm
import (
"strings"
)
// convertKVStringsToMap converts ["key=value"] to {"key":"value"}
func convertKVStringsToMap(values []string) map[string]string {
result := make(map[string]string, len(values))
for _, value := range values {
kv := strings.SplitN(value, "=", 2)
if len(kv) == 1 {
result[kv[0]] = ""
} else {
result[kv[0]] = kv[1]
}
}
return result
}
// convertMapToKVStrings converts {"key": "value"} to ["key=value"]
func convertMapToKVStrings(values map[string]string) []string {
result := make([]string, len(values))
i := 0
for key, value := range values {
result[i] = key + "=" + value
i++
}
return result
}

View File

@@ -0,0 +1,21 @@
package swarm
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConvertKVStringsToMap(t *testing.T) {
result := convertKVStringsToMap([]string{"HELLO=WORLD", "a=b=c=d", "e"})
expected := map[string]string{"HELLO": "WORLD", "a": "b=c=d", "e": ""}
assert.Equal(t, expected, result)
}
func TestConvertMapToKVStrings(t *testing.T) {
result := convertMapToKVStrings(map[string]string{"HELLO": "WORLD", "a": "b=c=d", "e": ""})
sort.Strings(result)
expected := []string{"HELLO=WORLD", "a=b=c=d", "e="}
assert.Equal(t, expected, result)
}

24
cluster/utils_darwin.go Normal file
View File

@@ -0,0 +1,24 @@
// +build darwin
package cluster
import (
"net"
"os"
"syscall"
"time"
"golang.org/x/sys/unix"
)
// setTCPUserTimeout sets TCP_RXT_CONNDROPTIME on darwin
func setTCPUserTimeout(conn *net.TCPConn, uto time.Duration) error {
f, err := conn.File()
if err != nil {
return err
}
defer f.Close()
secs := int(uto.Nanoseconds() / 1e9)
return os.NewSyscallError("setsockopt", syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_TCP, unix.TCP_RXT_CONNDROPTIME, secs))
}

28
cluster/utils_unix.go Normal file
View File

@@ -0,0 +1,28 @@
// +build linux
package cluster
import (
"net"
"os"
"syscall"
"time"
)
// setTCPUserTimeout sets TCP_USER_TIMEOUT according to RFC5842
func setTCPUserTimeout(conn *net.TCPConn, uto time.Duration) error {
f, err := conn.File()
if err != nil {
return err
}
defer f.Close()
msecs := int(uto.Nanoseconds() / 1e6)
// TCP_USER_TIMEOUT is a relatively new feature to detect dead peer from sender side.
// Linux supports it since kernel 2.6.37. It's among Golang experimental under
// golang.org/x/sys/unix but it doesn't support all Linux platforms yet.
// we explicitly define it here until it becomes official in golang.
// TODO: replace it with proper package when TCP_USER_TIMEOUT is supported in golang.
const tcpUserTimeout = 0x12
return os.NewSyscallError("setsockopt", syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_TCP, tcpUserTimeout, msecs))
}

16
cluster/utils_windows.go Normal file
View File

@@ -0,0 +1,16 @@
// +build !linux,!darwin
package cluster
import (
"errors"
"net"
"time"
)
// setTCPUserTimeout doesn't work under Windows because Go doesn't support
// the option and swarm doesn't support cgo
// This is a usability enhancement. Service shouldn't fail on this error.
func setTCPUserTimeout(conn *net.TCPConn, uto time.Duration) error {
return errors.New("Go doesn't have native support for TCP_USER_TIMEOUT for this platform")
}

52
cluster/volume.go Normal file
View File

@@ -0,0 +1,52 @@
package cluster
import "github.com/docker/engine-api/types"
// Volume is exported
type Volume struct {
types.Volume
Engine *Engine
}
// Volumes represents an array of volumes
type Volumes []*Volume
// Get returns a volume using its ID or Name
func (volumes Volumes) Get(name string) *Volume {
// Abort immediately if the name is empty.
if len(name) == 0 {
return nil
}
candidates := []*Volume{}
// Match name or engine/name.
for _, volume := range volumes {
if volume.Name == name || volume.Engine.ID+"/"+volume.Name == name || volume.Engine.Name+"/"+volume.Name == name {
candidates = append(candidates, volume)
}
}
// Return if we found a unique match.
if size := len(candidates); size == 1 {
return candidates[0]
} else if size > 1 {
// Match first volume with non-local driver
for _, volume := range candidates {
if volume.Name == name && volume.Driver != "local" {
return volume
}
}
return nil
}
// Match /name and return as soon as we find one.
for _, volume := range volumes {
if volume.Name == "/"+name {
return volume
}
}
return nil
}

90
cluster/volume_test.go Normal file
View File

@@ -0,0 +1,90 @@
package cluster
import (
"testing"
"github.com/docker/engine-api/types"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
engines := []*Engine{
{ID: "59C8F4EC-3A71-44C7-BA50-0496B4D8E548",
Name: "swarm-node-0"},
{ID: "607B1F15-CECA-41B2-9D26-D2626EB14D8D",
Name: "swarm-node-1"},
}
volumes := Volumes{
&Volume{
Volume: types.Volume{
Name: "t1",
Driver: "local",
},
Engine: engines[0],
},
&Volume{
Volume: types.Volume{
Name: "t2",
Driver: "local",
},
Engine: engines[0],
},
&Volume{
Volume: types.Volume{
Name: "t3",
Driver: "rexray",
},
Engine: engines[0],
},
&Volume{
Volume: types.Volume{
Name: "t4",
Driver: "flocker",
},
Engine: engines[0],
},
&Volume{
Volume: types.Volume{
Name: "t1",
Driver: "local",
},
Engine: engines[1],
},
&Volume{
Volume: types.Volume{
Name: "t2",
Driver: "local",
},
Engine: engines[1],
},
&Volume{
Volume: types.Volume{
Name: "t3",
Driver: "rexray",
},
Engine: engines[1],
},
&Volume{
Volume: types.Volume{
Name: "t4",
Driver: "flocker",
},
Engine: engines[1],
},
}
emptyVolumes := Volumes{}
assert.Nil(t, emptyVolumes.Get("test"))
assert.Nil(t, volumes.Get("t1"))
if assert.NotNil(t, volumes.Get(engines[0].ID+"/"+"t1")) {
assert.Equal(t, volumes.Get(engines[0].ID+"/"+"t1"), volumes[0])
}
if assert.NotNil(t, volumes.Get(engines[0].Name+"/"+"t1")) {
assert.Equal(t, volumes.Get(engines[0].Name+"/"+"t1"), volumes[0])
}
if assert.NotNil(t, volumes.Get("t3")) {
assert.Equal(t, volumes.Get("t3"), volumes[2])
}
if assert.NotNil(t, volumes.Get("t4")) {
assert.Equal(t, volumes.Get("t4"), volumes[3])
}
}

162
cluster/watchdog.go Normal file
View File

@@ -0,0 +1,162 @@
package cluster
import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types/network"
"golang.org/x/net/context"
)
// Watchdog listens to cluster events and handles container rescheduling
type Watchdog struct {
sync.Mutex
cluster Cluster
}
// Handle handles cluster callbacks
func (w *Watchdog) Handle(e *Event) error {
// Skip non-swarm events.
if e.From != "swarm" {
return nil
}
switch e.Status {
case "engine_connect", "engine_reconnect":
go w.removeDuplicateContainers(e.Engine)
case "engine_disconnect":
go w.rescheduleContainers(e.Engine)
}
return nil
}
// removeDuplicateContainers removes duplicate containers when a node comes back
func (w *Watchdog) removeDuplicateContainers(e *Engine) {
log.Debugf("removing duplicate containers from Node %s", e.ID)
e.RefreshContainers(false)
w.Lock()
defer w.Unlock()
for _, container := range e.Containers() {
// skip non-swarm containers
if container.Config.SwarmID() == "" {
continue
}
for _, containerInCluster := range w.cluster.Containers() {
if containerInCluster.Config.SwarmID() == container.Config.SwarmID() && containerInCluster.Engine.ID != container.Engine.ID {
log.Debugf("container %s was rescheduled on node %s, removing it", container.ID, containerInCluster.Engine.Name)
// container already exists in the cluster, destroy it
if err := e.RemoveContainer(container, true, true); err != nil {
log.Errorf("Failed to remove duplicate container %s on node %s: %v", container.ID, containerInCluster.Engine.Name, err)
}
}
}
}
}
// rescheduleContainers reschedules containers as soon as a node fails
func (w *Watchdog) rescheduleContainers(e *Engine) {
w.Lock()
defer w.Unlock()
log.Debugf("Node %s failed - rescheduling containers", e.ID)
for _, c := range e.Containers() {
// Skip containers which don't have an "on-node-failure" reschedule policy.
if !c.Config.HasReschedulePolicy("on-node-failure") {
log.Debugf("Skipping rescheduling of %s based on rescheduling policies", c.ID)
continue
}
// Remove the container from the dead engine. If we don't, then both
// the old and new one will show up in docker ps.
// We have to do this before calling `CreateContainer`, otherwise it
// will abort because the name is already taken.
c.Engine.removeContainer(c)
// keep track of all global networks this container is connected to
globalNetworks := make(map[string]*network.EndpointSettings)
// if the existing containter has global network endpoints,
// they need to be removed with force option
// "docker network disconnect -f network containername" only takes containername
name := c.Info.Name
if len(name) == 0 || len(name) == 1 && name[0] == '/' {
log.Errorf("container %s has no name", c.ID)
continue
}
// cut preceeding '/'
if name[0] == '/' {
name = name[1:]
}
if c.NetworkSettings != nil && len(c.NetworkSettings.Networks) > 0 {
// find an engine to do disconnect work
randomEngine, err := w.cluster.RANDOMENGINE()
if err != nil {
log.Errorf("Failed to find an engine to do network cleanup for container %s: %v", c.ID, err)
// add the container back, so we can retry later
c.Engine.AddContainer(c)
continue
}
clusterNetworks := w.cluster.Networks().Uniq()
for networkName, endpoint := range c.NetworkSettings.Networks {
net := clusterNetworks.Get(endpoint.NetworkID)
if net != nil && net.Scope == "global" {
// record the nework, they should be reconstructed on the new container
globalNetworks[networkName] = endpoint
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = randomEngine.apiClient.NetworkDisconnect(ctx, networkName, name, true)
if err != nil {
// do not abort here as this endpoint might have been removed before
log.Warnf("Failed to remove network endpoint from old container %s: %v", name, err)
}
}
}
}
newContainer, err := w.cluster.CreateContainer(c.Config, c.Info.Name, nil)
if err != nil {
log.Errorf("Failed to reschedule container %s: %v", c.ID, err)
// add the container back, so we can retry later
c.Engine.AddContainer(c)
continue
}
// Docker create command cannot create a container with multiple networks
// see https://github.com/docker/docker/issues/17750
// Add the global networks one by one
for networkName, endpoint := range globalNetworks {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = newContainer.Engine.apiClient.NetworkConnect(ctx, networkName, name, endpoint)
if err != nil {
log.Warnf("Failed to connect network %s to container %s: %v", networkName, name, err)
}
}
log.Infof("Rescheduled container %s from %s to %s as %s", c.ID, c.Engine.Name, newContainer.Engine.Name, newContainer.ID)
if c.Info.State.Running {
log.Infof("Container %s was running, starting container %s", c.ID, newContainer.ID)
if err := w.cluster.StartContainer(newContainer, nil); err != nil {
log.Errorf("Failed to start rescheduled container %s: %v", newContainer.ID, err)
}
}
}
}
// NewWatchdog creates a new watchdog
func NewWatchdog(cluster Cluster) *Watchdog {
log.Debugf("Watchdog enabled")
w := &Watchdog{
cluster: cluster,
}
cluster.RegisterEventHandler(w)
return w
}

8
discovery/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Contribute a new discovery backend
Docker Swarm comes with multiple Discovery backends. To read the end-user
documentation, visit the [Swarm discovery documentation on
docs.docker.com](https://docs.docker.com/swarm/discovery/). If you want to
modify the discovery end-user documentation, start with [the `docs/discovery.md`
file](https://github.com/docker/swarm/blob/master/docs/discovery.md) in this
project.

View File

@@ -0,0 +1,7 @@
#discovery.hub.docker.com
Docker Swarm comes with a simple discovery service built into the [Docker
Hub](http://hub.docker.com). To read the end-user API documentation, visit [the
Swarm discovery documentation on
docs.docker.com](https://docs.docker.com/swarm/discovery/). If you want to
modify the discovery documentation, start with [the `docs/discovery.md`]

142
discovery/token/token.go Normal file
View File

@@ -0,0 +1,142 @@
package token
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/docker/docker/pkg/discovery"
)
const discoveryURL = "https://discovery.hub.docker.com/v1"
// Discovery is exported
type Discovery struct {
heartbeat time.Duration
ttl time.Duration
url string
token string
}
func init() {
Init()
}
// Init is exported
func Init() {
discovery.Register("token", &Discovery{})
}
// Initialize is exported
func (s *Discovery) Initialize(urltoken string, heartbeat time.Duration, ttl time.Duration, _ map[string]string) error {
if i := strings.LastIndex(urltoken, "/"); i != -1 {
s.url = "https://" + urltoken[:i]
s.token = urltoken[i+1:]
} else {
s.url = discoveryURL
s.token = urltoken
}
if s.token == "" {
return errors.New("token is empty")
}
s.heartbeat = heartbeat
s.ttl = ttl
return nil
}
// Fetch returns the list of entries for the discovery service at the specified endpoint
func (s *Discovery) fetch() (discovery.Entries, error) {
resp, err := http.Get(fmt.Sprintf("%s/%s/%s", s.url, "clusters", s.token))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var addrs []string
if resp.StatusCode == http.StatusOK {
if err := json.NewDecoder(resp.Body).Decode(&addrs); err != nil {
return nil, fmt.Errorf("Failed to decode response: %v", err)
}
} else {
return nil, fmt.Errorf("Failed to fetch entries, Discovery service returned %d HTTP status code", resp.StatusCode)
}
return discovery.CreateEntries(addrs)
}
// Watch is exported
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
ch := make(chan discovery.Entries)
ticker := time.NewTicker(s.heartbeat)
errCh := make(chan error)
go func() {
defer close(ch)
defer close(errCh)
// Send the initial entries if available.
currentEntries, err := s.fetch()
if err != nil {
errCh <- err
} else {
ch <- currentEntries
}
// Periodically send updates.
for {
select {
case <-ticker.C:
newEntries, err := s.fetch()
if err != nil {
errCh <- err
continue
}
// Check if the file has really changed.
if !newEntries.Equals(currentEntries) {
ch <- newEntries
}
currentEntries = newEntries
case <-stopCh:
ticker.Stop()
return
}
}
}()
return ch, errCh
}
// Register adds a new entry identified by the into the discovery service
func (s *Discovery) Register(addr string) error {
buf := strings.NewReader(addr)
resp, err := http.Post(fmt.Sprintf("%s/%s/%s?ttl=%d", s.url,
"clusters", s.token, uint64(s.ttl.Seconds())), "application/json", buf)
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// CreateCluster returns a unique cluster token
func (s *Discovery) CreateCluster() (string, error) {
resp, err := http.Post(fmt.Sprintf("%s/%s", s.url, "clusters"), "", nil)
if err != nil {
return "", err
}
defer resp.Body.Close()
token, err := ioutil.ReadAll(resp.Body)
return string(token), err
}

View File

@@ -0,0 +1,48 @@
package token
import (
"testing"
"time"
"github.com/docker/docker/pkg/discovery"
"github.com/stretchr/testify/assert"
)
func TestInitialize(t *testing.T) {
discovery := &Discovery{}
err := discovery.Initialize("token", 0, 0, nil)
assert.NoError(t, err)
assert.Equal(t, discovery.token, "token")
assert.Equal(t, discovery.url, discoveryURL)
err = discovery.Initialize("custom/path/token", 0, 0, nil)
assert.NoError(t, err)
assert.Equal(t, discovery.token, "token")
assert.Equal(t, discovery.url, "https://custom/path")
err = discovery.Initialize("", 0, 0, nil)
assert.Error(t, err)
}
func TestRegister(t *testing.T) {
d := &Discovery{token: "TEST_TOKEN", url: discoveryURL, heartbeat: 1}
expected := "127.0.0.1:2675"
expectedEntries, err := discovery.CreateEntries([]string{expected})
assert.NoError(t, err)
// Register
assert.NoError(t, d.Register(expected))
// Watch
ch, errCh := d.Watch(nil)
select {
case entries := <-ch:
assert.True(t, entries.Equals(expectedEntries))
case err := <-errCh:
t.Fatal(err)
case <-time.After(5 * time.Second):
t.Fatal("Timed out")
}
assert.NoError(t, d.Register(expected))
}

8
docs/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM docs/base:oss
MAINTAINER Docker Docs <docs@docker.com>
ENV PROJECT=swarm
# to get the git info for this repo
COPY . /src
RUN rm -rf /docs/content/$PROJECT/
COPY . /docs/content/$PROJECT/

37
docs/Makefile Normal file
View File

@@ -0,0 +1,37 @@
.PHONY: all default docs docs-build docs-shell shell test
# to allow `make DOCSDIR=1 docs-shell` (to create a bind mount in docs)
DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/swarm/)
# to allow `make DOCSPORT=9000 docs`
DOCSPORT := 8000
# Get the IP ADDRESS
DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''")
HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)")
HUGO_BIND_IP=0.0.0.0
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g")
DOCKER_DOCS_IMAGE := docker-docs$(if $(GIT_BRANCH_CLEAN),:$(GIT_BRANCH_CLEAN))
DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE
# for some docs workarounds (see below in "docs-build" target)
GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
default: docs
docs: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) --watch
docs-draft: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
docs-shell: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash
test: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)"
docs-build:
docker build -t "$(DOCKER_DOCS_IMAGE)" .

564
docs/configure-tls.md Normal file
View File

@@ -0,0 +1,564 @@
<!--[metadata]>
+++
title = "Configure Docker Swarm for TLS"
description = "Swarm and transport layer security"
keywords = ["docker, swarm, TLS, discovery, security, certificates"]
[menu.main]
parent="workw_swarm"
weight=55
+++
<![end-metadata]-->
# Configure Docker Swarm for TLS
In this procedure you create a two-node Swarm cluster, a Docker Engine CLI, a
Swarm Manager, and a Certificate Authority as shown below. All the Docker Engine
hosts (`client`, `swarm`, `node1`, and `node2`) have a copy of the
CA's certificate as well as their own key-pair signed by the CA.
![](images/tls-1.jpg)
You will complete the following steps in this procedure:
- [Step 1: Set up the prerequisites](#step-1-set-up-the-prerequisites)
- [Step 2: Create a Certificate Authority (CA) server](#step-2-create-a-certificate-authority-ca-server)
- [Step 3: Create and sign keys](#step-3-create-and-sign-keys)
- [Step 4: Install the keys](#step-4-install-the-keys)
- [Step 5: Configure the Engine daemon for TLS](#step-5-configure-the-engine-daemon-for-tls)
- [Step 6: Create a Swarm cluster](#step-6-create-a-swarm-cluster)
- [Step 7: Create the Swarm Manager using TLS](#step-7-create-the-swarm-manager-using-tls)
- [Step 8: Test the Swarm manager configuration](#step-8-test-the-swarm-manager-configuration)
- [Step 9: Configure the Engine CLI to use TLS](#step-9-configure-the-engine-cli-to-use-tls)
### Before you begin
The article includes steps to create your own CA using OpenSSL. This is similar
to operating your own internal corporate CA and PKI. However, this `must not`
be used as a guide to building a production-worthy internal CA and PKI. These
steps are included for demonstration purposes only - so that readers without
access to an existing CA and set of certificates can follow along and configure
Docker Swarm to use TLS.
## Step 1: Set up the prerequisites
To complete this procedure you must stand up 5 (five) Linux servers. These
servers can be any mix of physical and virtual servers; they may be on premises
or in the public cloud. The following table lists each server name and its purpose.
| Server name | Description |
|-------------|------------------------------------------------|
| `ca` | Acts as the Certificate Authority (CA) server. |
| `swarm` | Acts as the Swarm Manager. |
| `node1` | Act as a Swarm node. |
| `node2` | Act as a Swarm node. |
| `client` | Acts as a remote Docker Engine client |
Make sure that you have SSH access to all 5 servers and that they can communicate with each other using DNS name resolution. In particular:
- Open TCP port 2376 between the Swarm Manager and Swarm nodes
- Open TCP port 3376 between the Docker Engine client and the Swarm Manager
You can choose different ports if these are already in use. This example assumes
you use these ports though.
Each server must run an operating system compatible with Docker Engine. For
simplicity, the steps that follow assume all servers are running Ubuntu 14.04
LTS.
## Step 2: Create a Certificate Authority (CA) server
>**Note**:If you already have access to a CA and certificates, and are comfortable working with them, you should skip this step and go to the next.
In this step, you configure a Linux server as a CA. You use this CA to create
and sign keys. This step included so that readers without access to an existing
CA (external or corporate) and certificates can follow along and complete the
later steps that require installing and using certificates. It is `not`
intended as a model for how to deploy production-worthy CA.
1. Logon to the terminal of your CA server and elevate to root.
$ sudo su
2. Create a private key called `ca-priv-key.pem` for the CA:
# openssl genrsa -out ca-priv-key.pem 2048
Generating RSA private key, 2048 bit long modulus
...........................................................+++
.....+++
e is 65537 (0x10001)
3. Create a public key called `ca.pem` for the CA.
The public key is based on the private key created in the previous step.
# openssl req -config /usr/lib/ssl/openssl.cnf -new -key ca-priv-key.pem -x509 -days 1825 -out ca.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
<output truncated>
You have now configured a CA server with a public and private keypair. You can inspect the contents of each key. To inspect the private key:
```
# openssl rsa -in ca-priv-key.pem -noout -text
```
To inspect the public key (cert):
```
# openssl x509 -in ca.pem -noout -text
```
The following command shows the partial contents of the CA's public key.
# openssl x509 -in ca.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 17432010264024107661 (0xf1eaf0f9f41eca8d)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=CA, L=Sanfrancisco, O=Docker Inc
Validity
Not Before: Jan 16 18:28:12 2016 GMT
Not After : Jan 13 18:28:12 2026 GMT
Subject: C=US, ST=CA, L=San Francisco, O=Docker Inc
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d1:fe:6e:55:d4:93:fc:c9:8a:04:07:2d:ba:f0:
55:97:c5:2c:f5:d7:1d:6a:9b:f0:f0:55:6c:5d:90:
<output truncated>
Later, you'll use this to certificate to sign keys for other servers in the
infrastructure.
## Step 3: Create and sign keys
Now that you have a working CA, you need to create key pairs for the Swarm
Manager, Swarm nodes, and remote Docker Engine client. The commands and process
to create key pairs is identical for all servers. You'll create the following keys:
<table>
<tr>
<th></th>
<th></th>
</tr>
<tr>
<td><code>ca-priv-key.pem</td>
<td>The CA's private key and must be kept secure. It is used later to sign new keys for the other nodes in the environment. Together with the <code>ca.pem</code> file, this makes up the CA's key pair.</td>
</tr>
<tr>
<td><code>ca.pem</td>
<td>The CA's public key (also called certificate). This is installed on all nodes in the environment so that all nodes trust certificates signed by the CA. Together with the <code>ca-priv-key.pem</code> file, this makes up the CA's key pair.</td>
</tr>
<tr>
<td><code><i>node</i>.csr</code></td>
<td>A certificate signing request (CSR). A CSR is effectively an application to the CA to create a new key pair for a particular node. The CA takes the information provided in the CSR and generates the public and private key pair for that node.</td>
</tr>
<tr>
<td><code><i>node</i>-priv-key.pem</code></td>
<td>A private key signed by the CA. The node uses this key to authenticate itself with remote Docker Engines. Together with the <code><i>node</i>-cert.pem</code> file, this makes up a node's key pair.</td>
</tr>
<tr>
<td><code><i>node</i>-cert.pem</code></td>
<td>A certificate signed by the CA. This is not used in this example. Together with the <code><i>node</i>-priv-key.pem</code> file, this makes up a node's key pair</td>
</tr>
</table>
The commands below show how to create keys for all of your nodes. You perform this procedure in a working directory located on your CA server.
1. Logon to the terminal of your CA server and elevate to root.
$ sudo su
2. Create a private key `swarm-priv-key.pem` for your Swarm Manager
# openssl genrsa -out swarm-priv-key.pem 2048
Generating RSA private key, 2048 bit long modulus
............................................................+++
........+++
e is 65537 (0x10001)
2. Generate a certificate signing request (CSR) `swarm.csr` using the private key you create in the previous step.
# openssl req -subj "/CN=swarm" -new -key swarm-priv-key.pem -out swarm.csr
Remember, this is only for demonstration purposes. The process to create a
CSR will be slightly different in real-world production environments.
3. Create the certificate `swarm-cert.pem` based on the CSR created in the previous step.
# openssl x509 -req -days 1825 -in swarm.csr -CA ca.pem -CAkey ca-priv-key.pem -CAcreateserial -out swarm-cert.pem -extensions v3_req -extfile /usr/lib/ssl/openssl.cnf
<snip>
# openssl rsa -in swarm-priv-key.pem -out swarm-priv-key.pem
You now have a keypair for the Swarm Manager.
4. Repeat the steps above for the remaining nodes in your infrastructure (`node1`, `node2`, and `client`).
Remember to replace the `swarm` specific values with the values relevant to the node you are creating the key pair for.
<table>
<tr>
<th>Server name</th>
<th>Private key</th>
<th>CSR</th>
<th>Certificate</th>
</tr>
<tr>
<td><code>node1 </code></td>
<td><code>node1-priv-key.pem</code></td>
<td><code>node1.csr</code></td>
<td><code>node1-cert.pem</code></td>
</tr>
<tr>
<td><code>node2</code></td>
<td><code>node2-priv-key.pem</code></td>
<td><code>node2.csr</code></td>
<td><code>node2-cert.pem</code></td>
</tr>
<tr>
<td><code>client</code></td>
<td><code>client-priv-key.pem</td>
<td><code>client.csr</code></td>
<td><code>client-cert.pem</code></td>
</tr>
</table>
5. Verify that your working directory contains the following files:
# ls -l
total 64
-rw-r--r-- 1 root root 1679 Jan 16 18:27 ca-priv-key.pem
-rw-r--r-- 1 root root 1229 Jan 16 18:28 ca.pem
-rw-r--r-- 1 root root 17 Jan 18 09:56 ca.srl
-rw-r--r-- 1 root root 1086 Jan 18 09:56 client-cert.pem
-rw-r--r-- 1 root root 887 Jan 18 09:55 client.csr
-rw-r--r-- 1 root root 1679 Jan 18 09:56 client-priv-key.pem
-rw-r--r-- 1 root root 1082 Jan 18 09:44 node1-cert.pem
-rw-r--r-- 1 root root 887 Jan 18 09:43 node1.csr
-rw-r--r-- 1 root root 1675 Jan 18 09:44 node1-priv-key.pem
-rw-r--r-- 1 root root 1082 Jan 18 09:49 node2-cert.pem
-rw-r--r-- 1 root root 887 Jan 18 09:49 node2.csr
-rw-r--r-- 1 root root 1675 Jan 18 09:49 node2-priv-key.pem
-rw-r--r-- 1 root root 1082 Jan 18 09:42 swarm-cert.pem
-rw-r--r-- 1 root root 887 Jan 18 09:41 swarm.csr
-rw-r--r-- 1 root root 1679 Jan 18 09:42 swarm-priv-key.pem
You can inspect the contents of each of the keys. To inspect a private key:
```
openssl rsa -in <key-name> -noout -text
```
To inspect a public key (cert):
```
openssl x509 -in <key-name> -noout -text
```
The following commands shows the partial contents of the Swarm Manager's public
`swarm-cert.pem` key.
```
# openssl x509 -in ca.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 9590646456311914051 (0x8518d2237ad49e43)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=CA, L=Sanfrancisco, O=Docker Inc
Validity
Not Before: Jan 18 09:42:16 2016 GMT
Not After : Jan 15 09:42:16 2026 GMT
Subject: CN=swarm
<output truncated>
```
## Step 4: Install the keys
In this step, you install the keys on the relevant servers in the
infrastructure. Each server needs three files:
- A copy of the Certificate Authority's public key (`ca.pem`)
- It's own private key
- It's own public key (cert)
The procedure below shows you how to copy these files from the CA server to each
server using `scp`. As part of the copy procedure, you'll rename each file as
follows on each node:
| Original name | Copied name |
|-------------------------|-------------|
| `ca.pem` | `ca.pem` |
| `<server>-cert.pem` | `cert.pem` |
| `<server>-priv-key.pem` | `key.pem` |
1. Logon to the terminal of your CA server and elevate to root.
$ sudo su
2. Create a` ~/.certs` directory on the Swarm manager. Here we assume user account is ubuntu.
$ ssh ubuntu@swarm 'mkdir -p /home/ubuntu/.certs'
2. Copy the keys from the CA to the Swarm Manager server.
$ scp ./ca.pem ubuntu@swarm:/home/ubuntu/.certs/ca.pem
$ scp ./swarm-cert.pem ubuntu@swarm:/home/ubuntu/.certs/cert.pem
$ scp ./swarm-priv-key.pem ubuntu@swarm:/home/ubuntu/.certs/key.pem
>**Note**: You may need to provide authentication for the `scp` commands to work. For example, AWS EC2 instances use certificate-based authentication. To copy the files to an EC2 instance associated with a public key called `nigel.pem`, modify the `scp` command as follows: `scp -i /path/to/nigel.pem ./ca.pem ubuntu@swarm:/home/ubuntu/.certs/ca.pem`.
3. Repeat step 2 for each remaining server in the infrastructure.
* `node1`
* `node2`
* `client`
4. Verify your work.
When the copying is complete, each machine should have the following keys.
![](images/tls-2.jpeg)
Each node in your infrastructure should have the following files in the
`/home/ubuntu/.certs/` directory:
# ls -l /home/ubuntu/.certs/
total 16
-rw-r--r-- 1 ubuntu ubuntu 1229 Jan 18 10:03 ca.pem
-rw-r--r-- 1 ubuntu ubuntu 1082 Jan 18 10:06 cert.pem
-rw-r--r-- 1 ubuntu ubuntu 1679 Jan 18 10:06 key.pem
## Step 5: Configure the Engine daemon for TLS
In the last step, you created and installed the necessary keys on each of your
Swarm nodes. In this step, you configure them to listen on the network and only
accept connections using TLS. Once you complete this step, your Swarm nodes will
listen on TCP port 2376, and only accept connections using TLS.
On `node1` and `node2` (your Swarm nodes), do the following:
1. Open a terminal on `node1` and elevate to root.
$ sudo su
2. Edit Docker Engine configuration file.
If you are following along with these instructions and using Ubuntu 14.04
LTS, the configuration file is `/etc/default/docker`. The Docker Engine
configuration file may be different depending on the Linux distribution you
are using.
3. Add the following options to the `DOCKER_OPTS` line.
-H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/home/ubuntu/.certs/ca.pem --tlscert=/home/ubuntu/.certs/cert.pem --tlskey=/home/ubuntu/.certs/key.pem
2. Restart the Docker Engine daemon.
$ service docker restart
3. Repeat the procedure on `node2` as well.
## Step 6: Create a Swarm cluster
Next create a Swarm cluster. In this procedure you create a two-node Swarm
cluster using the default *hosted discovery* backend. The default hosted
discovery backend uses Docker Hub and is not recommended for production use.
1. Logon to the terminal of your Swarm manager node.
2. Create the cluster and export it's unique ID to the `TOKEN` environment variable.
$ sudo export TOKEN=$(docker run --rm swarm create)
Unable to find image 'swarm:latest' locally
latest: Pulling from library/swarm
d681c900c6e3: Pulling fs layer
<snip>
986340ab62f0: Pull complete
a9975e2cc0a3: Pull complete
Digest: sha256:c21fd414b0488637b1f05f13a59b032a3f9da5d818d31da1a4ca98a84c0c781b
Status: Downloaded newer image for swarm:latest
3. Join `node1` to the cluster.
Be sure to specify TCP port `2376` and not `2375`.
$ sudo docker run -d swarm join --addr=node1:2376 token://$TOKEN
7bacc98536ed6b4200825ff6f4004940eb2cec891e1df71c6bbf20157c5f9761
4. Join `node2` to the cluster.
$ sudo docker run -d swarm join --addr=node2:2376 token://$TOKEN
db3f49d397bad957202e91f0679ff84f526e74d6c5bf1b6734d834f5edcbca6c
## Step 7: Start the Swarm Manager using TLS
1. Launch a new container with TLS enables
$ docker run -d -p 3376:3376 -v /home/ubuntu/.certs:/certs:ro swarm manage --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/cert.pem --tlskey=/certs/key.pem --host=0.0.0.0:3376 token://$TOKEN
The command above launches a new container based on the `swarm` image
and it maps port `3376` on the server to port `3376` inside the
container. This mapping ensures that Docker Engine commands sent to the host
on port `3376` are passed on to port `3376` inside the container. The
container runs the Swarm `manage` process with the `--tlsverify`,
`--tlscacert`, `--tlscert` and `--tlskey` options specified. These options
force TLS verification and specify the location of the Swarm manager's TLS
keys.
2. Run a `docker ps` command to verify that your Swarm manager container is up
and running.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
035dbf57b26e swarm "/swarm manage --tlsv" 7 seconds ago Up 7 seconds 2375/tcp, 0.0.0.0:3376->3376/tcp compassionate_lovelace
Your Swarm cluster is now configured to use TLS.
## Step 8: Test the Swarm manager configuration
Now that you have a Swarm cluster built and configured to use TLS, you'll test that it works with a Docker Engine CLI.
1. Open a terminal onto your `client` server.
2. Issue the `docker version` command.
When issuing the command, you must pass it the location of the clients certifications.
$ sudo docker --tlsverify --tlscacert=/home/ubuntu/.certs/ca.pem --tlscert=/home/ubuntu/.certs/cert.pem --tlskey=/home/ubuntu/.certs/key.pem -H swarm:3376 version
Client:
Version: 1.9.1
API version: 1.21
Go version: go1.4.2
Git commit: a34a1d5
Built: Fri Nov 20 13:12:04 UTC 2015
OS/Arch: linux/amd64
Server:
Version: swarm/1.0.1
API version: 1.21
Go version: go1.5.2
Git commit: 744e3a3
Built:
OS/Arch: linux/amd64
The output above shows the `Server` version as "swarm/1.0.1". This means
that the command was successfully issued against the Swarm manager.
2. Verify that the same command does not work without TLS.
This time, do not pass your certs to the Swarm manager.
$ sudo docker -H swarm:3376 version
:
Version: 1.9.1
API version: 1.21
Go version: go1.4.2
Git commit: a34a1d5
Built: Fri Nov 20 13:12:04 UTC 2015
OS/Arch: linux/amd64
Get http://swarm:3376/v1.21/version: malformed HTTP response "\x15\x03\x01\x00\x02\x02".
* Are you trying to connect to a TLS-enabled daemon without TLS?
The output above shows that the command was rejected by the server. This is
because the server (Swarm manager) is configured to only accept connections
from authenticated clients using TLS.
## Step 9: Configure the Engine CLI to use TLS
You can configure the Engine so that you don't have to pass the TLS options when
you issue a command. To do this, you'll configure the `Docker Engine host` and
`TLS` settings as defaults on your Docker Engine client.
To do this, you place the client's keys in your `~/.docker` configuration folder. If you have other users on your system using the Engine command line, you'll need to configure their account's `~/.docker` as well. The procedure below shows how to do this for the `ubuntu` user on
your Docker Engine client.
1. Open a terminal onto your `client` server.
2. If it doesn't exist, create a `.docker` directory in the `ubuntu` user's home directory.
$ mkdir /home/ubuntu/.docker
4. Copy the Docker Engine client's keys from `/home/ubuntu/.certs` to
`/home/ubuntu/.docker`
$ cp /home/ubuntu/.certs/{ca,cert,key}.pem /home/ubuntu/.docker
5. Edit the account's `~/.bash_profile`.
6. Set the following variables:
<table>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
<tr>
<td><code>DOCKER_HOST</code></td>
<td>Sets the Docker host and TCP port to send all Engine commands to.</td>
</tr>
<tr>
<td><code>DOCKER_TLS_VERIFY</code></td>
<td>Tell's Engine to use TLS.</td>
</tr>
<tr>
<td><code>DOCKER_CERT_PATH</code></td>
<td>Specifies the location of TLS keys.</td>
</tr>
</table>
For example:
export DOCKER_HOST=tcp://swarm:3376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=/home/ubuntu/.docker/
6. Save and close the file.
7. Source the file to pick up the new variables.
$ source ~/.bash_profile
8. Verify that the procedure worked by issuing a `docker version` command
$ docker version
Client:
Version: 1.9.1
API version: 1.21
Go version: go1.4.2
Git commit: a34a1d5
Built: Fri Nov 20 13:12:04 UTC 2015
OS/Arch: linux/amd64
Server:
Version: swarm/1.0.1
API version: 1.21
Go version: go1.5.2
Git commit: 744e3a3
Built:
OS/Arch: linux/amd64
The server portion of the output above command shows that your Docker
client is issuing commands to the Swarm Manager and using TLS.
Congratulations! You have configured a Docker Swarm cluster to use TLS.
## Related Information
* [Secure Docker Swarm with TLS](secure-swarm-tls.md)
* [Docker security](https://docs.docker.com/engine/security/security/)

225
docs/discovery.md Normal file
View File

@@ -0,0 +1,225 @@
<!--[metadata]>
+++
title = "Discovery"
description = "Swarm discovery"
keywords = ["docker, swarm, clustering, discovery"]
[menu.main]
parent="workw_swarm"
weight=4
+++
<![end-metadata]-->
# Docker Swarm Discovery
Docker Swarm comes with multiple discovery backends. You use a hosted discovery service with Docker Swarm. The service maintains a list of IPs in your cluster.
This page describes the different types of hosted discovery available to you. These are:
## Using a distributed key/value store
The recommended way to do node discovery in Swarm is Docker's libkv project. The libkv project is an abstraction layer over existing distributed key/value stores. As of this writing, the project supports:
* Consul 0.5.1 or higher
* Etcd 2.0 or higher
* ZooKeeper 3.4.5 or higher
For details about libkv and a detailed technical overview of the supported backends, refer to the [libkv project](https://github.com/docker/libkv).
### Using a hosted discovery key store
1. On each node, start the Swarm agent.
The node IP address doesn't have to be public as long as the Swarm manager can access it. In a large cluster, the nodes joining swarm may trigger request spikes to discovery. For example, a large number of nodes are added by a script, or recovered from a network partition. This may result in discovery failure. You can use `--delay` option to specify a delay limit. Swarm join will add a random delay less than this limit to reduce pressure to discovery.
**Etcd**:
swarm join --advertise=<node_ip:2375> etcd://<etcd_addr1>,<etcd_addr2>/<optional path prefix>
**Consul**:
swarm join --advertise=<node_ip:2375> consul://<consul_addr>/<optional path prefix>
**ZooKeeper**:
swarm join --advertise=<node_ip:2375> zk://<zookeeper_addr1>,<zookeeper_addr2>/<optional path prefix>
2. Start the Swarm manager on any machine or your laptop.
**Etcd**:
swarm manage -H tcp://<swarm_ip:swarm_port> etcd://<etcd_addr1>,<etcd_addr2>/<optional path prefix>
**Consul**:
swarm manage -H tcp://<swarm_ip:swarm_port> consul://<consul_addr>/<optional path prefix>
**ZooKeeper**:
swarm manage -H tcp://<swarm_ip:swarm_port> zk://<zookeeper_addr1>,<zookeeper_addr2>/<optional path prefix>
4. Use the regular Docker commands.
docker -H tcp://<swarm_ip:swarm_port> info
docker -H tcp://<swarm_ip:swarm_port> run ...
docker -H tcp://<swarm_ip:swarm_port> ps
docker -H tcp://<swarm_ip:swarm_port> logs ...
...
5. Try listing the nodes in your cluster.
**Etcd**:
swarm list etcd://<etcd_addr1>,<etcd_addr2>/<optional path prefix>
<node_ip:2375>
**Consul**:
swarm list consul://<consul_addr>/<optional path prefix>
<node_ip:2375>
**ZooKeeper**:
swarm list zk://<zookeeper_addr1>,<zookeeper_addr2>/<optional path prefix>
<node_ip:2375>
### Use TLS with distributed key/value discovery
You can securely talk to the distributed k/v store using TLS. To connect
securely to the store, you must generate the certificates for a node when you
`join` it to the swarm. You can only use with Consul and Etcd. The following example illustrates this with Consul:
```
swarm join \
--advertise=<node_ip:2375> \
--discovery-opt kv.cacertfile=/path/to/mycacert.pem \
--discovery-opt kv.certfile=/path/to/mycert.pem \
--discovery-opt kv.keyfile=/path/to/mykey.pem \
consul://<consul_addr>/<optional path prefix>
```
This works the same way for the Swarm `manage` and `list` commands.
## A static file or list of nodes
> **Note***: This discovery method is incompatible with replicating Swarm
managers. If you require replication, you should use a hosted discovery key
store.
You can use a static file or list of nodes for your discovery backend. The file must be stored on a host that is accessible from the Swarm manager. You can also pass a node list as an option when you start Swarm.
Both the static file and the `nodes` option support an IP address ranges. To specify a range supply a pattern, for example, `10.0.0.[10:200]` refers to nodes starting from `10.0.0.10` to `10.0.0.200`. For example for the `file` discovery method.
$ echo "10.0.0.[11:100]:2375" >> /tmp/my_cluster
$ echo "10.0.1.[15:20]:2375" >> /tmp/my_cluster
$ echo "192.168.1.2:[2:20]375" >> /tmp/my_cluster
Or with node discovery:
swarm manage -H <swarm_ip:swarm_port> "nodes://10.0.0.[10:200]:2375,10.0.1.[2:250]:2375"
### To create a file
1. Edit the file and add line for each of your nodes.
echo <node_ip1:2375> >> /opt/my_cluster
echo <node_ip2:2375> >> /opt/my_cluster
echo <node_ip3:2375> >> /opt/my_cluster
This example creates a file named `/tmp/my_cluster`. You can use any name you like.
2. Start the Swarm manager on any machine.
swarm manage -H tcp://<swarm_ip:swarm_port> file:///tmp/my_cluster
3. Use the regular Docker commands.
docker -H tcp://<swarm_ip:swarm_port> info
docker -H tcp://<swarm_ip:swarm_port> run ...
docker -H tcp://<swarm_ip:swarm_port> ps
docker -H tcp://<swarm_ip:swarm_port> logs ...
...
4. List the nodes in your cluster.
$ swarm list file:///tmp/my_cluster
<node_ip1:2375>
<node_ip2:2375>
<node_ip3:2375>
### To use a node list
1. Start the manager on any machine or your laptop.
swarm manage -H <swarm_ip:swarm_port> nodes://<node_ip1:2375>,<node_ip2:2375>
or
swarm manage -H <swarm_ip:swarm_port> <node_ip1:2375>,<node_ip2:2375>
2. Use the regular Docker commands.
docker -H <swarm_ip:swarm_port> info
docker -H <swarm_ip:swarm_port> run ...
docker -H <swarm_ip:swarm_port> ps
docker -H <swarm_ip:swarm_port> logs ...
3. List the nodes in your cluster.
$ swarm list file:///tmp/my_cluster
<node_ip1:2375>
<node_ip2:2375>
<node_ip3:2375>
## Docker Hub as a hosted discovery service
> **Warning**: The Docker Hub Hosted Discovery Service **is not recommended** for production use. It's intended to be used for testing/development. See the discovery backends for production use.
This example uses the hosted discovery service on Docker Hub. Using
Docker Hub's hosted discovery service requires that each node in the
swarm is connected to the public internet. To create your cluster:
1. Create a cluster.
$ swarm create
6856663cdefdec325839a4b7e1de38e8 # <- this is your unique <cluster_id>
2. Create each node and join them to the cluster.
On each of your nodes, start the swarm agent. The node IP address doesn't have to be public (eg. 192.168.0.X) but the the Swarm manager must be able to access it.
$ swarm join --advertise=<node_ip:2375> token://<cluster_id>
3. Start the Swarm manager.
This can be on any machine or even your laptop.
$ swarm manage -H tcp://<swarm_ip:swarm_port> token://<cluster_id>
4. Use regular Docker commands to interact with your cluster.
docker -H tcp://<swarm_ip:swarm_port> info
docker -H tcp://<swarm_ip:swarm_port> run ...
docker -H tcp://<swarm_ip:swarm_port> ps
docker -H tcp://<swarm_ip:swarm_port> logs ...
...
5. List the nodes in your cluster.
swarm list token://<cluster_id>
<node_ip:2375>
## Contribute a new discovery backend
You can contribute a new discovery backend to Swarm. For information on how to
do this, see <a
href="https://github.com/docker/docker/tree/master/pkg/discovery">
github.com/docker/docker/pkg/discovery</a>.
## Docker Swarm documentation index
- [Docker Swarm overview](index.md)
- [Scheduler strategies](scheduler/strategy.md)
- [Scheduler filters](scheduler/filter.md)
- [Swarm API](swarm-api.md)

127
docs/get-swarm.md Normal file
View File

@@ -0,0 +1,127 @@
<!--[metadata]>
+++
title = "How to get Swarm"
description = "Running a Swarm container on Docker Engine. Run a Swarm binary on the host OS without Docker Engine."
keywords = ["docker, Swarm, container, binary, clustering, install, installation"]
[menu.main]
identifier="how-to-get-and-run-Swarm"
parent="workw_swarm"
weight=-90
+++
<![end-metadata]-->
# How to get Docker Swarm
You can create a Docker Swarm cluster using the `swarm` executable image from a
container or using an executable `swarm` binary you install on your system. This
page introduces the two methods and discusses their pros and cons.
## Create a cluster with an interactive container
You can use the Docker Swarm official image to create a cluster. The image is
built by Docker and updated regularly through an automated build. To use the
image, you run it a container via the Engine `docker run` command. The image has
multiple options and subcommands you can use to create and manage a Swarm cluster.
The first time you use any image, Docker Engine checks to see if you already have the image in your environment. By default Docker runs the `swarm:latest` version but you can also specify a tag other than `latest`. If you have an image locally but a newer one exists on Docker Hub, Engine downloads it.
### Run the Swarm image from a container
1. Open a terminal on a host running Engine.
If you are using Mac or Windows, then you must make sure you have started a Docker Engine host running and pointed your terminal environment to it with the Docker Machine commands. If you aren't sure, you can verify:
$ docker-machine ls
NAME ACTIVE URL STATE URL SWARM DOCKER ERRORS
default * virtualbox Running tcp://192.168.99.100:2376 v1.9.1
This shows an environment running an Engine host on the `default` instance.
2. Use the `swarm` image to execute a command.
The easiest command is to get the help for the image. This command shows all the options that are available with the image.
$ docker run swarm --help
Unable to find image 'swarm:latest' locally
latest: Pulling from library/swarm
d681c900c6e3: Pull complete
188de6f24f3f: Pull complete
90b2ffb8d338: Pull complete
237af4efea94: Pull complete
3b3fc6f62107: Pull complete
7e6c9135b308: Pull complete
986340ab62f0: Pull complete
a9975e2cc0a3: Pull complete
Digest: sha256:c21fd414b0488637b1f05f13a59b032a3f9da5d818d31da1a4ca98a84c0c781b
Status: Downloaded newer image for swarm:latest
Usage: swarm [OPTIONS] COMMAND [arg...]
A Docker-native clustering system
Version: 1.0.1 (744e3a3)
Options:
--debug debug mode [$DEBUG]
--log-level, -l "info" Log level (options: debug, info, warn, error, fatal, panic)
--help, -h show help
--version, -v print the version
Commands:
create, c Create a cluster
list, l List nodes in a cluster
manage, m Manage a docker cluster
join, j join a docker cluster
help, h Shows a list of commands or help for one command
Run 'swarm COMMAND --help' for more information on a command.
In this example, the `swarm` image did not exist on the Engine host, so the
Engine downloaded it. After it downloaded, the image executed the `help`
subcommand to display the help text. After displaying the help, the `swarm`
image exits and returns you to your terminal command line.
3. List the running containers on your Engine host.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Swarm is no longer running. The `swarm` image exits after you issue it a command.
### Why use the image?
Using a Swarm container has three key benefits over other methods:
* You don't need to install a binary on the system to use the image.
* The single command `docker run` command gets and run the most recent version of the image every time.
* The container isolates Swarm from your host environment. You don't need to perform or maintain shell paths and environments.
Running the Swarm image is the recommended way to create and manage your Swarm cluster. All of Docker's documentation and tutorials use this method.
## Run a Swarm binary
Before you run a Swarm binary directly on a host operating system (OS), you compile the binary from the source code or get a trusted copy from another location. Then you run the Swarm binary.
To compile Swarm from source code, refer to the instructions in
[CONTRIBUTING.md](http://github.com/docker/swarm/blob/master/CONTRIBUTING.md).
### Why use the binary?
Using a Swarm binary this way has one key benefit over other methods: If you are
a developer who contributes to the Swarm project, you can test your code changes
without "containerizing" the binary before you run it.
Running a Swarm binary on the host OS has disadvantages:
* Compilation from source is a burden.
* The binary doesn't have the benefits that
Docker containers provide, such as isolation.
* Most Docker documentation and tutorials don't show this method of running swarm.
Lastly, because the Swarm nodes don't use Engine, you can't use Docker-based
software tools, such as Docker Engine CLI at the node level.
## Related information
* [Docker Swarm official image](https://hub.docker.com/_/swarm/) repository on Docker Hub
* [Provision a Swarm with Docker Machine](provision-with-machine.md)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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