mirror of
https://github.com/docker/docs.git
synced 2026-11-05 00:14:17 +03:00
Initial import of https://github.com/docker/swarm
This commit is contained in:
11
.github/ISSUE_TEMPLATE.md
vendored
11
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,11 +0,0 @@
|
||||
## Current behavior
|
||||
|
||||
|
||||
## Expected behavior
|
||||
|
||||
|
||||
##### Version affected
|
||||
|
||||
|
||||
##### Docs referenced / affected
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,11 +0,0 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
.coverage*
|
||||
/.tox
|
||||
/build
|
||||
/coverage-html
|
||||
/dist
|
||||
/docs/_site
|
||||
/venv
|
||||
README.rst
|
||||
compose/GITSHA
|
||||
29
.travis.yml
29
.travis.yml
@@ -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
325
CHANGELOG.md
Normal 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
236
CONTRIBUTING.md
Normal 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
22
Dockerfile
Normal 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
386
Godeps/Godeps.json
generated
Normal 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
5
Godeps/Readme
generated
Normal 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
191
LICENSE.code
Normal 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
425
LICENSE.docs
Normal 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
120
MAINTAINERS
Normal 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
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Swarm: a Docker-native clustering system
|
||||
|
||||
[](https://godoc.org/github.com/docker/swarm)
|
||||
[](https://jenkins.dockerproject.org/view/Swarm/job/Swarm%20Master/)
|
||||
[](https://travis-ci.org/docker/swarm)
|
||||
[](https://goreportcard.com/report/github.com/docker/swarm)
|
||||
|
||||

|
||||
|
||||
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
76
RELEASE-CHECKLIST.md
Normal 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
50
ROADMAP.md
Normal 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
8
api/README.md
Normal 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
144
api/events.go
Normal 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
69
api/events_test.go
Normal 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
43
api/flusher.go
Normal 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
1373
api/handlers.go
Normal file
File diff suppressed because it is too large
Load Diff
406
api/mockclient/mock.go
Normal file
406
api/mockclient/mock.go
Normal 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)
|
||||
}
|
||||
36
api/mockclient/mock_test.go
Normal file
36
api/mockclient/mock_test.go
Normal 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
347
api/nopclient/nop.go
Normal 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
26
api/nopclient/nop_test.go
Normal 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
182
api/primary.go
Normal 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
81
api/primary_test.go
Normal 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
52
api/replica.go
Normal 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
124
api/server.go
Normal 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
36
api/server_unix.go
Normal 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
13
api/server_windows.go
Normal 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
46
api/sorter.go
Normal 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
7
api/status.go
Normal 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
336
api/utils.go
Normal 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
76
api/utils_test.go
Normal 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
79
cli/cli.go
Normal 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
43
cli/commands.go
Normal 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
22
cli/create.go
Normal 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
154
cli/flags.go
Normal 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
57
cli/help.go
Normal 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
83
cli/join.go
Normal 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
36
cli/join_test.go
Normal 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
41
cli/list.go
Normal 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
329
cli/manage.go
Normal 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
102
cluster/cluster.go
Normal 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
250
cluster/config.go
Normal 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
106
cluster/config_test.go
Normal 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
157
cluster/container.go
Normal 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
60
cluster/container_test.go
Normal 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
1402
cluster/engine.go
Normal file
File diff suppressed because it is too large
Load Diff
25
cluster/engine_sorter.go
Normal file
25
cluster/engine_sorter.go
Normal 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)
|
||||
}
|
||||
32
cluster/engine_sorter_test.go
Normal file
32
cluster/engine_sorter_test.go
Normal 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
847
cluster/engine_test.go
Normal 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
66
cluster/event.go
Normal 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
85
cluster/event_monitor.go
Normal 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
57
cluster/httpclient.go
Normal 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
126
cluster/image.go
Normal 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
140
cluster/image_test.go
Normal 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
150
cluster/mesos/README.md
Normal 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
79
cluster/mesos/agent.go
Normal 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
100
cluster/mesos/agent_test.go
Normal 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
719
cluster/mesos/cluster.go
Normal 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
|
||||
}
|
||||
95
cluster/mesos/cluster_test.go
Normal file
95
cluster/mesos/cluster_test.go
Normal 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")
|
||||
}
|
||||
23
cluster/mesos/offer_sorter.go
Normal file
23
cluster/mesos/offer_sorter.go
Normal 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()
|
||||
}
|
||||
24
cluster/mesos/offer_sorter_test.go
Normal file
24
cluster/mesos/offer_sorter_test.go
Normal 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
146
cluster/mesos/scheduler.go
Normal 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
239
cluster/mesos/task/task.go
Normal 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
|
||||
}
|
||||
79
cluster/mesos/task/task_test.go
Normal file
79
cluster/mesos/task/task_test.go
Normal 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)
|
||||
}
|
||||
61
cluster/mesos/task/tasks.go
Normal file
61
cluster/mesos/task/tasks.go
Normal 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())
|
||||
}
|
||||
}
|
||||
56
cluster/mesos/task/tasks_test.go
Normal file
56
cluster/mesos/task/tasks_test.go
Normal 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)
|
||||
|
||||
}
|
||||
16
cluster/mesos/task/utils.go
Normal file
16
cluster/mesos/task/utils.go
Normal 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
39
cluster/mesos/utils.go
Normal 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
163
cluster/network.go
Normal 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
191
cluster/network_test.go
Normal 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
69
cluster/options.go
Normal 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
154
cluster/options_test.go
Normal 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
995
cluster/swarm/cluster.go
Normal 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
|
||||
}
|
||||
270
cluster/swarm/cluster_test.go
Normal file
270
cluster/swarm/cluster_test.go
Normal 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
31
cluster/swarm/utils.go
Normal 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
|
||||
}
|
||||
21
cluster/swarm/utils_test.go
Normal file
21
cluster/swarm/utils_test.go
Normal 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
24
cluster/utils_darwin.go
Normal 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
28
cluster/utils_unix.go
Normal 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
16
cluster/utils_windows.go
Normal 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
52
cluster/volume.go
Normal 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
90
cluster/volume_test.go
Normal 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
162
cluster/watchdog.go
Normal 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
8
discovery/README.md
Normal 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.
|
||||
7
discovery/token/README.md
Normal file
7
discovery/token/README.md
Normal 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
142
discovery/token/token.go
Normal 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
|
||||
}
|
||||
48
discovery/token/token_test.go
Normal file
48
discovery/token/token_test.go
Normal 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
8
docs/Dockerfile
Normal 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
37
docs/Makefile
Normal 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
564
docs/configure-tls.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
225
docs/discovery.md
Normal 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
127
docs/get-swarm.md
Normal 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)
|
||||
1
docs/images/app-architecture.gliffy
Normal file
1
docs/images/app-architecture.gliffy
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/images/app-architecture.jpg
Normal file
BIN
docs/images/app-architecture.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/images/app-architecture.png
Normal file
BIN
docs/images/app-architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/images/aws-infrastructure.jpg
Normal file
BIN
docs/images/aws-infrastructure.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
docs/images/balance-examples.jpg
Normal file
BIN
docs/images/balance-examples.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
docs/images/cloud-formation-tmp.jpg
Normal file
BIN
docs/images/cloud-formation-tmp.jpg
Normal file
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
Reference in New Issue
Block a user