Compare commits

..

1 Commits
release ... py2

Author SHA1 Message Date
Nicolas De Loof
9ad10575d1 Prepare drop of python 2.x support
see https://github.com/docker/compose/issues/6890

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2019-11-20 16:00:53 +01:00
49 changed files with 1134 additions and 1440 deletions

63
.circleci/config.yml Normal file
View File

@@ -0,0 +1,63 @@
version: 2
jobs:
test:
macos:
xcode: "9.4.1"
steps:
- checkout
- run:
name: setup script
command: ./script/setup/osx
- run:
name: install tox
command: sudo pip install --upgrade tox==2.1.1 virtualenv==16.2.0
- run:
name: unit tests
command: tox -e py27,py37 -- tests/unit
build-osx-binary:
macos:
xcode: "9.4.1"
steps:
- checkout
- run:
name: upgrade python tools
command: sudo pip install --upgrade pip virtualenv==16.2.0
- run:
name: setup script
command: DEPLOYMENT_TARGET=10.11 ./script/setup/osx
- run:
name: build script
command: ./script/build/osx
- store_artifacts:
path: dist/docker-compose-Darwin-x86_64
destination: docker-compose-Darwin-x86_64
- deploy:
name: Deploy binary to bintray
command: |
OS_NAME=Darwin PKG_NAME=osx ./script/circle/bintray-deploy.sh
build-linux-binary:
machine:
enabled: true
steps:
- checkout
- run:
name: build Linux binary
command: ./script/build/linux
- store_artifacts:
path: dist/docker-compose-Linux-x86_64
destination: docker-compose-Linux-x86_64
- deploy:
name: Deploy binary to bintray
command: |
OS_NAME=Linux PKG_NAME=linux ./script/circle/bintray-deploy.sh
workflows:
version: 2
all:
jobs:
- test
- build-linux-binary
- build-osx-binary

View File

@@ -11,4 +11,3 @@ docs/_site
.tox
**/__pycache__
*.pyc
Jenkinsfile

6
.github/CODEOWNERS vendored
View File

@@ -1,6 +0,0 @@
# GitHub code owners
# See https://help.github.com/articles/about-codeowners/
#
# KEEP THIS FILE SORTED. Order is important. Last match takes precedence.
* @ndeloof @rumpl @ulyssessouza

View File

@@ -1,171 +1,6 @@
Change log
==========
1.25.2 (2020-01-17)
-------------------
### Features
- Allow compatibility option with `COMPOSE_COMPATIBILITY` environment variable
- Bump PyInstaller from 3.5 to 3.6
- Bump pysocks from 1.6.7 to 1.7.1
- Bump websocket-client from 0.32.0 to 0.57.0
- Bump urllib3 from 1.24.2 to 1.25.7
- Bump jsonschema from 3.0.1 to 3.2.0
- Bump PyYAML from 4.2b1 to 5.3
- Bump certifi from 2017.4.17 to 2019.11.28
- Bump coverage from 4.5.4 to 5.0.3
- Bump paramiko from 2.6.0 to 2.7.1
- Bump cached-property from 1.3.0 to 1.5.1
- Bump minor Linux and MacOSX dependencies
### Bugfixes
- Validate version format on formats 2+
- Assume infinite terminal width when not running in a terminal
1.25.1 (2020-01-06)
-------------------
### Features
- Bump `pytest-cov` 2.8.1
- Bump `flake8` 3.7.9
- Bump `coverage` 4.5.4
### Bugfixes
- Decode APIError explanation to unicode before usage on start and create of a container
- Reports when images that cannot be pulled and must be built
- Discard label `com.docker.compose.filepaths` having None as value. Typically, when coming from stdin
- Added OSX binary as a directory to solve slow start up time caused by MacOS Catalina binary scan
- Passed in HOME env-var in container mode (running with `script/run/run.sh`)
- Reverted behavior of "only pull images that we can't build" and replace by a warning informing the image we can't pull and must be built
1.25.0 (2019-11-18)
-------------------
### Features
- Set no-colors to true if CLICOLOR env variable is set to 0
- Add working dir, config files and env file in service labels
- Add dependencies for ARM build
- Add BuildKit support, use `DOCKER_BUILDKIT=1` and `COMPOSE_DOCKER_CLI_BUILD=1`
- Bump paramiko to 2.6.0
- Add working dir, config files and env file in service labels
- Add tag `docker-compose:latest`
- Add `docker-compose:<version>-alpine` image/tag
- Add `docker-compose:<version>-debian` image/tag
- Bumped `docker-py` 4.1.0
- Supports `requests` up to 2.22.0 version
- Drops empty tag on `build:cache_from`
- `Dockerfile` now generates `libmusl` binaries for alpine
- Only pull images that can't be built
- Attribute `scale` can now accept `0` as a value
- Added `--quiet` build flag
- Added `--no-interpolate` to `docker-compose config`
- Bump OpenSSL for macOS build (`1.1.0j` to `1.1.1c`)
- Added `--no-rm` to `build` command
- Added support for `credential_spec`
- Resolve digests without pulling image
- Upgrade `pyyaml` to `4.2b1`
- Lowered severity to `warning` if `down` tries to remove nonexisting image
- Use improved API fields for project events when possible
- Update `setup.py` for modern `pypi/setuptools` and remove `pandoc` dependencies
- Removed `Dockerfile.armhf` which is no longer needed
### Bugfixes
- Make container service color deterministic, remove red from chosen colors
- Fix non ascii chars error. Python2 only
- Format image size as decimal to be align with Docker CLI
- Use Python Posix support to get tty size
- Fix same file 'extends' optimization
- Use python POSIX support to get tty size
- Format image size as decimal to be align with Docker CLI
- Fixed stdin_open
- Fixed `--remove-orphans` when used with `up --no-start`
- Fixed `docker-compose ps --all`
- Fixed `depends_on` dependency recreation behavior
- Fixed bash completion for `build --memory`
- Fixed misleading warning concerning env vars when performing an `exec` command
- Fixed failure check in parallel_execute_watch
- Fixed race condition after pulling image
- Fixed error on duplicate mount points
- Fixed merge on networks section
- Always connect Compose container to `stdin`
- Fixed the presentation of failed services on 'docker-compose start' when containers are not available
1.24.1 (2019-06-24)
-------------------
### Bugfixes
- Fixed acceptance tests
1.24.0 (2019-03-28)
-------------------

View File

@@ -1,9 +1,9 @@
ARG DOCKER_VERSION=19.03.5
ARG PYTHON_VERSION=3.7.5
ARG DOCKER_VERSION=18.09.7
ARG PYTHON_VERSION=3.7.4
ARG BUILD_ALPINE_VERSION=3.10
ARG BUILD_DEBIAN_VERSION=slim-stretch
ARG RUNTIME_ALPINE_VERSION=3.10.3
ARG RUNTIME_DEBIAN_VERSION=stretch-20191118-slim
ARG RUNTIME_ALPINE_VERSION=3.10.1
ARG RUNTIME_DEBIAN_VERSION=stretch-20190812-slim
ARG BUILD_PLATFORM=alpine

194
Jenkinsfile vendored
View File

@@ -1,112 +1,92 @@
#!groovy
def dockerVersions = ['19.03.5', '18.09.9']
def buildImage = { String baseImage ->
def image
wrappedNode(label: "ubuntu && amd64 && !zfs", cleanWorkspace: true) {
stage("build image for \"${baseImage}\"") {
checkout(scm)
def imageName = "dockerbuildbot/compose:${baseImage}-${gitCommit()}"
image = docker.image(imageName)
try {
image.pull()
} catch (Exception exc) {
sh """GIT_COMMIT=\$(script/build/write-git-sha) && \\
docker build -t ${imageName} \\
--target build \\
--build-arg BUILD_PLATFORM="${baseImage}" \\
--build-arg GIT_COMMIT="${GIT_COMMIT}" \\
.\\
"""
sh "docker push ${imageName}"
echo "${imageName}"
return imageName
}
}
}
echo "image.id: ${image.id}"
return image.id
}
def get_versions = { String imageId, int number ->
def docker_versions
wrappedNode(label: "ubuntu && amd64 && !zfs") {
def result = sh(script: """docker run --rm \\
--entrypoint=/code/.tox/py37/bin/python \\
${imageId} \\
/code/script/test/versions.py -n ${number} docker/docker-ce recent
""", returnStdout: true
)
docker_versions = result.split()
}
return docker_versions
}
def runTests = { Map settings ->
def dockerVersions = settings.get("dockerVersions", null)
def pythonVersions = settings.get("pythonVersions", null)
def baseImage = settings.get("baseImage", null)
def imageName = settings.get("image", null)
if (!pythonVersions) {
throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py37')`")
}
if (!dockerVersions) {
throw new Exception("Need Docker versions to test. e.g.: `runTests(dockerVersions: 'all')`")
}
{ ->
wrappedNode(label: "ubuntu && amd64 && !zfs", cleanWorkspace: true) {
stage("test python=${pythonVersions} / docker=${dockerVersions} / baseImage=${baseImage}") {
checkout(scm)
def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim()
echo "Using local system's storage driver: ${storageDriver}"
sh """docker run \\
-t \\
--rm \\
--privileged \\
--volume="\$(pwd)/.git:/code/.git" \\
--volume="/var/run/docker.sock:/var/run/docker.sock" \\
-e "TAG=${imageName}" \\
-e "STORAGE_DRIVER=${storageDriver}" \\
-e "DOCKER_VERSIONS=${dockerVersions}" \\
-e "BUILD_NUMBER=\$BUILD_TAG" \\
-e "PY_TEST_VERSIONS=${pythonVersions}" \\
--entrypoint="script/test/ci" \\
${imageName} \\
--verbose
"""
}
}
}
}
def testMatrix = [failFast: true]
def baseImages = ['alpine', 'debian']
def pythonVersions = ['py27', 'py37']
pipeline {
agent none
options {
skipDefaultCheckout(true)
buildDiscarder(logRotator(daysToKeepStr: '30'))
timeout(time: 2, unit: 'HOURS')
timestamps()
}
stages {
stage('Build test images') {
// TODO use declarative 1.5.0 `matrix` once available on CI
parallel {
stage('alpine') {
agent {
label 'ubuntu && amd64 && !zfs'
}
steps {
buildImage('alpine')
}
}
stage('debian') {
agent {
label 'ubuntu && amd64 && !zfs'
}
steps {
buildImage('debian')
}
}
}
}
stage('Test') {
steps {
// TODO use declarative 1.5.0 `matrix` once available on CI
script {
def testMatrix = [:]
baseImages.each { baseImage ->
dockerVersions.each { dockerVersion ->
pythonVersions.each { pythonVersion ->
testMatrix["${baseImage}_${dockerVersion}_${pythonVersion}"] = runTests(dockerVersion, pythonVersion, baseImage)
}
}
}
parallel testMatrix
}
}
}
}
baseImages.each { baseImage ->
def imageName = buildImage(baseImage)
get_versions(imageName, 2).each { dockerVersion ->
testMatrix["${baseImage}_${dockerVersion}"] = runTests([baseImage: baseImage, image: imageName, dockerVersions: dockerVersion, pythonVersions: 'py37'])
}
}
def buildImage(baseImage) {
def scmvar = checkout(scm)
def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}"
image = docker.image(imageName)
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
try {
image.pull()
} catch (Exception exc) {
ansiColor('xterm') {
sh """docker build -t ${imageName} \\
--target build \\
--build-arg BUILD_PLATFORM="${baseImage}" \\
--build-arg GIT_COMMIT="${scmvar.GIT_COMMIT}" \\
.\\
"""
sh "docker push ${imageName}"
}
echo "${imageName}"
return imageName
}
}
}
def runTests(dockerVersion, pythonVersion, baseImage) {
return {
stage("python=${pythonVersion} docker=${dockerVersion} ${baseImage}") {
node("ubuntu && amd64 && !zfs") {
def scmvar = checkout(scm)
def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}"
def storageDriver = sh(script: "docker info -f \'{{.Driver}}\'", returnStdout: true).trim()
echo "Using local system's storage driver: ${storageDriver}"
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
sh """docker run \\
-t \\
--rm \\
--privileged \\
--volume="\$(pwd)/.git:/code/.git" \\
--volume="/var/run/docker.sock:/var/run/docker.sock" \\
-e "TAG=${imageName}" \\
-e "STORAGE_DRIVER=${storageDriver}" \\
-e "DOCKER_VERSIONS=${dockerVersion}" \\
-e "BUILD_NUMBER=${env.BUILD_NUMBER}" \\
-e "PY_TEST_VERSIONS=${pythonVersion}" \\
--entrypoint="script/test/ci" \\
${imageName} \\
--verbose
"""
}
}
}
}
}
parallel(testMatrix)

View File

@@ -11,7 +11,6 @@
[Org]
[Org."Core maintainers"]
people = [
"ndeloof",
"rumpl",
"ulyssessouza",
]
@@ -78,11 +77,6 @@
Email = "mazz@houseofmnowster.com"
GitHub = "mnowster"
[people.ndeloof]
Name = "Nicolas De Loof"
Email = "nicolas.deloof@gmail.com"
GitHub = "ndeloof"
[people.rumpl]
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"

View File

@@ -2,8 +2,6 @@ Docker Compose
==============
![Docker Compose](logo.png?raw=true "Docker Compose Logo")
## :exclamation: The docker-compose project announces that as Python 2 reaches it's EOL, versions 1.25.x will be the last to support it. For more information, please refer to this [issue](https://github.com/docker/compose/issues/6890).
Compose is a tool for defining and running multi-container Docker applications.
With Compose, you use a Compose file to configure your application's services.
Then, using a single command, you create and start all the services

View File

@@ -1,315 +0,0 @@
#!groovy
def dockerVersions = ['19.03.5', '18.09.9']
def baseImages = ['alpine', 'debian']
def pythonVersions = ['py27', 'py37']
pipeline {
agent none
options {
skipDefaultCheckout(true)
buildDiscarder(logRotator(daysToKeepStr: '30'))
timeout(time: 2, unit: 'HOURS')
timestamps()
}
stages {
stage('Build test images') {
// TODO use declarative 1.5.0 `matrix` once available on CI
parallel {
stage('alpine') {
agent {
label 'linux'
}
steps {
buildImage('alpine')
}
}
stage('debian') {
agent {
label 'linux'
}
steps {
buildImage('debian')
}
}
}
}
stage('Test') {
steps {
// TODO use declarative 1.5.0 `matrix` once available on CI
script {
def testMatrix = [:]
baseImages.each { baseImage ->
dockerVersions.each { dockerVersion ->
pythonVersions.each { pythonVersion ->
testMatrix["${baseImage}_${dockerVersion}_${pythonVersion}"] = runTests(dockerVersion, pythonVersion, baseImage)
}
}
}
parallel testMatrix
}
}
}
stage('Generate Changelog') {
agent {
label 'linux'
}
steps {
checkout scm
withCredentials([string(credentialsId: 'github-compose-release-test-token', variable: 'GITHUB_TOKEN')]) {
sh "./script/release/generate_changelog.sh"
}
archiveArtifacts artifacts: 'CHANGELOG.md'
stash( name: "changelog", includes: 'CHANGELOG.md' )
}
}
stage('Package') {
parallel {
stage('macosx binary') {
agent {
label 'mac-python'
}
steps {
checkout scm
sh './script/setup/osx'
sh 'tox -e py27,py37 -- tests/unit'
sh './script/build/osx'
dir ('dist') {
checksum('docker-compose-Darwin-x86_64')
checksum('docker-compose-Darwin-x86_64.tgz')
}
archiveArtifacts artifacts: 'dist/*', fingerprint: true
dir("dist") {
stash name: "bin-darwin"
}
}
}
stage('linux binary') {
agent {
label 'linux'
}
steps {
checkout scm
sh ' ./script/build/linux'
dir ('dist') {
checksum('docker-compose-Linux-x86_64')
}
archiveArtifacts artifacts: 'dist/*', fingerprint: true
dir("dist") {
stash name: "bin-linux"
}
}
}
stage('windows binary') {
agent {
label 'windows-python'
}
environment {
PATH = "$PATH;C:\\Python37;C:\\Python37\\Scripts"
}
steps {
checkout scm
bat 'tox.exe -e py27,py37 -- tests/unit'
powershell '.\\script\\build\\windows.ps1'
dir ('dist') {
checksum('docker-compose-Windows-x86_64.exe')
}
archiveArtifacts artifacts: 'dist/*', fingerprint: true
dir("dist") {
stash name: "bin-win"
}
}
}
stage('alpine image') {
agent {
label 'linux'
}
steps {
buildRuntimeImage('alpine')
}
}
stage('debian image') {
agent {
label 'linux'
}
steps {
buildRuntimeImage('debian')
}
}
}
}
stage('Release') {
when {
buildingTag()
}
parallel {
stage('Pushing images') {
agent {
label 'linux'
}
steps {
pushRuntimeImage('alpine')
pushRuntimeImage('debian')
}
}
stage('Creating Github Release') {
agent {
label 'linux'
}
steps {
checkout scm
sh 'mkdir -p dist'
dir("dist") {
unstash "bin-darwin"
unstash "bin-linux"
unstash "bin-win"
unstash "changelog"
githubRelease()
}
}
}
stage('Publishing Python packages') {
agent {
label 'linux'
}
steps {
checkout scm
withCredentials([[$class: "FileBinding", credentialsId: 'pypirc-docker-dsg-cibot', variable: 'PYPIRC']]) {
sh """
virtualenv venv-publish
source venv-publish/bin/activate
python setup.py sdist bdist_wheel
pip install twine
twine upload --config-file ${PYPIRC} ./dist/docker-compose-${env.TAG_NAME}.tar.gz ./dist/docker_compose-${env.TAG_NAME}-py2.py3-none-any.whl
"""
}
}
post {
sh 'deactivate; rm -rf venv-publish'
}
}
}
}
}
}
def buildImage(baseImage) {
def scmvar = checkout(scm)
def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}"
image = docker.image(imageName)
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
try {
image.pull()
} catch (Exception exc) {
ansiColor('xterm') {
sh """docker build -t ${imageName} \\
--target build \\
--build-arg BUILD_PLATFORM="${baseImage}" \\
--build-arg GIT_COMMIT="${scmvar.GIT_COMMIT}" \\
.\\
"""
sh "docker push ${imageName}"
}
echo "${imageName}"
return imageName
}
}
}
def runTests(dockerVersion, pythonVersion, baseImage) {
return {
stage("python=${pythonVersion} docker=${dockerVersion} ${baseImage}") {
node("linux") {
def scmvar = checkout(scm)
def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}"
def storageDriver = sh(script: "docker info -f \'{{.Driver}}\'", returnStdout: true).trim()
echo "Using local system's storage driver: ${storageDriver}"
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
sh """docker run \\
-t \\
--rm \\
--privileged \\
--volume="\$(pwd)/.git:/code/.git" \\
--volume="/var/run/docker.sock:/var/run/docker.sock" \\
-e "TAG=${imageName}" \\
-e "STORAGE_DRIVER=${storageDriver}" \\
-e "DOCKER_VERSIONS=${dockerVersion}" \\
-e "BUILD_NUMBER=${env.BUILD_NUMBER}" \\
-e "PY_TEST_VERSIONS=${pythonVersion}" \\
--entrypoint="script/test/ci" \\
${imageName} \\
--verbose
"""
}
}
}
}
}
def buildRuntimeImage(baseImage) {
scmvar = checkout scm
def imageName = "docker/compose:${baseImage}-${env.BRANCH_NAME}"
ansiColor('xterm') {
sh """docker build -t ${imageName} \\
--build-arg BUILD_PLATFORM="${baseImage}" \\
--build-arg GIT_COMMIT="${scmvar.GIT_COMMIT.take(7)}" \\
.
"""
}
sh "mkdir -p dist"
sh "docker save ${imageName} -o dist/docker-compose-${baseImage}.tar"
stash name: "compose-${baseImage}", includes: "dist/docker-compose-${baseImage}.tar"
}
def pushRuntimeImage(baseImage) {
unstash "compose-${baseImage}"
sh 'echo -n "${DOCKERHUB_CREDS_PSW}" | docker login --username "${DOCKERHUB_CREDS_USR}" --password-stdin'
sh "docker load -i dist/docker-compose-${baseImage}.tar"
withDockerRegistry(credentialsId: 'dockerbuildbot-hub.docker.com') {
sh "docker push docker/compose:${baseImage}-${env.TAG_NAME}"
if (baseImage == "alpine" && env.TAG_NAME != null) {
sh "docker tag docker/compose:alpine-${env.TAG_NAME} docker/compose:${env.TAG_NAME}"
sh "docker push docker/compose:${env.TAG_NAME}"
}
}
}
def githubRelease() {
withCredentials([string(credentialsId: 'github-compose-release-test-token', variable: 'GITHUB_TOKEN')]) {
def prerelease = !( env.TAG_NAME ==~ /v[0-9\.]+/ )
changelog = readFile "CHANGELOG.md"
def data = """{
\"tag_name\": \"${env.TAG_NAME}\",
\"name\": \"${env.TAG_NAME}\",
\"draft\": true,
\"prerelease\": ${prerelease},
\"body\" : \"${changelog}\"
"""
echo $data
def url = "https://api.github.com/repos/docker/compose/releases"
def upload_url = sh(returnStdout: true, script: """
curl -sSf -H 'Authorization: token ${GITHUB_TOKEN}' -H 'Accept: application/json' -H 'Content-type: application/json' -X POST -d '$data' $url") \\
| jq '.upload_url | .[:rindex("{")]'
""")
sh("""
for f in * ; do
curl -sf -H 'Authorization: token ${GITHUB_TOKEN}' -H 'Accept: application/json' -H 'Content-type: application/octet-stream' \\
-X POST --data-binary @\$f ${upload_url}?name=\$f;
done
""")
}
}
def checksum(filepath) {
if (isUnix()) {
sh "openssl sha256 -r -out ${filepath}.sha256 ${filepath}"
} else {
powershell "(Get-FileHash -Path ${filepath} -Algorithm SHA256 | % hash) + ' *${filepath}' > ${filepath}.sha256"
}
}

24
appveyor.yml Normal file
View File

@@ -0,0 +1,24 @@
version: '{branch}-{build}'
install:
- "SET PATH=C:\\Python37-x64;C:\\Python37-x64\\Scripts;%PATH%"
- "python --version"
- "pip install tox==2.9.1 virtualenv==16.2.0"
# Build the binary after tests
build: false
test_script:
- "tox -e py27,py37 -- tests/unit"
- ps: ".\\script\\build\\windows.ps1"
artifacts:
- path: .\dist\docker-compose-Windows-x86_64.exe
name: "Compose Windows binary"
deploy:
- provider: Environment
name: master-builds
on:
branch: master

View File

@@ -1,4 +1,4 @@
from __future__ import absolute_import
from __future__ import unicode_literals
__version__ = '1.25.2-rc1'
__version__ = '1.25.0dev'

275
compose/bundle.py Normal file
View File

@@ -0,0 +1,275 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import json
import logging
import six
from docker.utils import split_command
from docker.utils.ports import split_port
from .cli.errors import UserError
from .config.serialize import denormalize_config
from .network import get_network_defs_for_service
from .service import format_environment
from .service import NoSuchImageError
from .service import parse_repository_tag
log = logging.getLogger(__name__)
SERVICE_KEYS = {
'working_dir': 'WorkingDir',
'user': 'User',
'labels': 'Labels',
}
IGNORED_KEYS = {'build'}
SUPPORTED_KEYS = {
'image',
'ports',
'expose',
'networks',
'command',
'environment',
'entrypoint',
} | set(SERVICE_KEYS)
VERSION = '0.1'
class NeedsPush(Exception):
def __init__(self, image_name):
self.image_name = image_name
class NeedsPull(Exception):
def __init__(self, image_name, service_name):
self.image_name = image_name
self.service_name = service_name
class MissingDigests(Exception):
def __init__(self, needs_push, needs_pull):
self.needs_push = needs_push
self.needs_pull = needs_pull
def serialize_bundle(config, image_digests):
return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
def get_image_digests(project, allow_push=False):
digests = {}
needs_push = set()
needs_pull = set()
for service in project.services:
try:
digests[service.name] = get_image_digest(
service,
allow_push=allow_push,
)
except NeedsPush as e:
needs_push.add(e.image_name)
except NeedsPull as e:
needs_pull.add(e.service_name)
if needs_push or needs_pull:
raise MissingDigests(needs_push, needs_pull)
return digests
def get_image_digest(service, allow_push=False):
if 'image' not in service.options:
raise UserError(
"Service '{s.name}' doesn't define an image tag. An image name is "
"required to generate a proper image digest for the bundle. Specify "
"an image repo and tag with the 'image' option.".format(s=service))
_, _, separator = parse_repository_tag(service.options['image'])
# Compose file already uses a digest, no lookup required
if separator == '@':
return service.options['image']
digest = get_digest(service)
if digest:
return digest
if 'build' not in service.options:
raise NeedsPull(service.image_name, service.name)
if not allow_push:
raise NeedsPush(service.image_name)
return push_image(service)
def get_digest(service):
digest = None
try:
image = service.image()
# TODO: pick a digest based on the image tag if there are multiple
# digests
if image['RepoDigests']:
digest = image['RepoDigests'][0]
except NoSuchImageError:
try:
# Fetch the image digest from the registry
distribution = service.get_image_registry_data()
if distribution['Descriptor']['digest']:
digest = '{image_name}@{digest}'.format(
image_name=service.image_name,
digest=distribution['Descriptor']['digest']
)
except NoSuchImageError:
raise UserError(
"Digest not found for service '{service}'. "
"Repository does not exist or may require 'docker login'"
.format(service=service.name))
return digest
def push_image(service):
try:
digest = service.push()
except Exception:
log.error(
"Failed to push image for service '{s.name}'. Please use an "
"image tag that can be pushed to a Docker "
"registry.".format(s=service))
raise
if not digest:
raise ValueError("Failed to get digest for %s" % service.name)
repo, _, _ = parse_repository_tag(service.options['image'])
identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
# only do this if RepoDigests isn't already populated
image = service.image()
if not image['RepoDigests']:
# Pull by digest so that image['RepoDigests'] is populated for next time
# and we don't have to pull/push again
service.client.pull(identifier)
log.info("Stored digest for {}".format(service.image_name))
return identifier
def to_bundle(config, image_digests):
if config.networks:
log.warning("Unsupported top level key 'networks' - ignoring")
if config.volumes:
log.warning("Unsupported top level key 'volumes' - ignoring")
config = denormalize_config(config)
return {
'Version': VERSION,
'Services': {
name: convert_service_to_bundle(
name,
service_dict,
image_digests[name],
)
for name, service_dict in config['services'].items()
},
}
def convert_service_to_bundle(name, service_dict, image_digest):
container_config = {'Image': image_digest}
for key, value in service_dict.items():
if key in IGNORED_KEYS:
continue
if key not in SUPPORTED_KEYS:
log.warning("Unsupported key '{}' in services.{} - ignoring".format(key, name))
continue
if key == 'environment':
container_config['Env'] = format_environment({
envkey: envvalue for envkey, envvalue in value.items()
if envvalue
})
continue
if key in SERVICE_KEYS:
container_config[SERVICE_KEYS[key]] = value
continue
set_command_and_args(
container_config,
service_dict.get('entrypoint', []),
service_dict.get('command', []))
container_config['Networks'] = make_service_networks(name, service_dict)
ports = make_port_specs(service_dict)
if ports:
container_config['Ports'] = ports
return container_config
# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95
def set_command_and_args(config, entrypoint, command):
if isinstance(entrypoint, six.string_types):
entrypoint = split_command(entrypoint)
if isinstance(command, six.string_types):
command = split_command(command)
if entrypoint:
config['Command'] = entrypoint + command
return
if command:
config['Args'] = command
def make_service_networks(name, service_dict):
networks = []
for network_name, network_def in get_network_defs_for_service(service_dict).items():
for key in network_def.keys():
log.warning(
"Unsupported key '{}' in services.{}.networks.{} - ignoring"
.format(key, name, network_name))
networks.append(network_name)
return networks
def make_port_specs(service_dict):
ports = []
internal_ports = [
internal_port
for port_def in service_dict.get('ports', [])
for internal_port in split_port(port_def)[0]
]
internal_ports += service_dict.get('expose', [])
for internal_port in internal_ports:
spec = make_port_spec(internal_port)
if spec not in ports:
ports.append(spec)
return ports
def make_port_spec(value):
components = six.text_type(value).partition('/')
return {
'Protocol': components[2] or 'tcp',
'Port': int(components[0]),
}

View File

@@ -40,8 +40,7 @@ SILENT_COMMANDS = {
}
def project_from_options(project_dir, options, additional_options=None):
additional_options = additional_options or {}
def project_from_options(project_dir, options, additional_options={}):
override_dir = options.get('--project-directory')
environment_file = options.get('--env-file')
environment = Environment.from_env_file(override_dir or project_dir, environment_file)
@@ -60,7 +59,7 @@ def project_from_options(project_dir, options, additional_options=None):
tls_config=tls_config_from_options(options, environment),
environment=environment,
override_dir=override_dir,
compatibility=compatibility_from_options(project_dir, options, environment),
compatibility=options.get('--compatibility'),
interpolate=(not additional_options.get('--no-interpolate')),
environment_file=environment_file
)
@@ -82,8 +81,7 @@ def set_parallel_limit(environment):
parallel.GlobalLimit.set_global_limit(parallel_limit)
def get_config_from_options(base_dir, options, additional_options=None):
additional_options = additional_options or {}
def get_config_from_options(base_dir, options, additional_options={}):
override_dir = options.get('--project-directory')
environment_file = options.get('--env-file')
environment = Environment.from_env_file(override_dir or base_dir, environment_file)
@@ -92,7 +90,7 @@ def get_config_from_options(base_dir, options, additional_options=None):
)
return config.load(
config.find(base_dir, config_path, environment, override_dir),
compatibility_from_options(config_path, options, environment),
options.get('--compatibility'),
not additional_options.get('--no-interpolate')
)
@@ -161,25 +159,15 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
def execution_context_labels(config_details, environment_file):
extra_labels = [
'{0}={1}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir))
'{0}={1}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir)),
'{0}={1}'.format(LABEL_CONFIG_FILES, config_files_label(config_details)),
]
if not use_config_from_stdin(config_details):
extra_labels.append('{0}={1}'.format(LABEL_CONFIG_FILES, config_files_label(config_details)))
if environment_file is not None:
extra_labels.append('{0}={1}'.format(LABEL_ENVIRONMENT_FILE,
os.path.normpath(environment_file)))
return extra_labels
def use_config_from_stdin(config_details):
for c in config_details.config_files:
if not c.filename:
return True
return False
def config_files_label(config_details):
return ",".join(
map(str, (os.path.normpath(c.filename) for c in config_details.config_files)))
@@ -200,13 +188,3 @@ def get_project_name(working_dir, project_name=None, environment=None):
return normalize_name(project)
return 'default'
def compatibility_from_options(working_dir, options=None, environment=None):
"""Get compose v3 compatibility from --compatibility option
or from COMPOSE_COMPATIBILITY environment variable."""
compatibility_option = options.get('--compatibility')
compatibility_environment = environment.get_boolean('COMPOSE_COMPATIBILITY')
return compatibility_option or compatibility_environment

View File

@@ -17,12 +17,7 @@ else:
def get_tty_width():
try:
# get_terminal_size can't determine the size if compose is piped
# to another command. But in such case it doesn't make sense to
# try format the output by terminal size as this output is consumed
# by another command. So let's pretend we have a huge terminal so
# output is single-lined
width, _ = get_terminal_size(fallback=(999, 0))
width, _ = get_terminal_size()
return int(width)
except OSError:
return 0

View File

@@ -15,12 +15,14 @@ from distutils.spawn import find_executable
from inspect import getdoc
from operator import attrgetter
import docker.errors
import docker.utils
import docker
from . import errors
from . import signals
from .. import __version__
from ..bundle import get_image_digests
from ..bundle import MissingDigests
from ..bundle import serialize_bundle
from ..config import ConfigurationError
from ..config import parse_environment
from ..config import parse_labels
@@ -32,8 +34,6 @@ from ..const import COMPOSEFILE_V2_2 as V2_2
from ..const import IS_WINDOWS_PLATFORM
from ..errors import StreamParseError
from ..progress_stream import StreamOutputError
from ..project import get_image_digests
from ..project import MissingDigests
from ..project import NoSuchService
from ..project import OneOffFilter
from ..project import ProjectError
@@ -213,6 +213,7 @@ class TopLevelCommand(object):
Commands:
build Build or rebuild services
bundle Generate a Docker bundle from the Compose file
config Validate and view the Compose file
create Create services
down Stop and remove containers, networks, images, and volumes
@@ -303,6 +304,38 @@ class TopLevelCommand(object):
progress=options.get('--progress'),
)
def bundle(self, options):
"""
Generate a Distributed Application Bundle (DAB) from the Compose file.
Images must have digests stored, which requires interaction with a
Docker registry. If digests aren't stored for all images, you can fetch
them with `docker-compose pull` or `docker-compose push`. To push images
automatically when bundling, pass `--push-images`. Only services with
a `build` option specified will have their images pushed.
Usage: bundle [options]
Options:
--push-images Automatically push images for any services
which have a `build` option specified.
-o, --output PATH Path to write the bundle file to.
Defaults to "<project name>.dab".
"""
compose_config = get_config_from_options('.', self.toplevel_options)
output = options["--output"]
if not output:
output = "{}.dab".format(self.project.name)
image_digests = image_digests_for_project(self.project, options['--push-images'])
with open(output, 'w') as f:
f.write(serialize_bundle(compose_config, image_digests))
log.info("Wrote bundle to {}".format(output))
def config(self, options):
"""
Validate and view the Compose file.
@@ -1012,7 +1045,6 @@ class TopLevelCommand(object):
--build Build images before starting containers.
--abort-on-container-exit Stops all containers if any container was
stopped. Incompatible with -d.
--attach-dependencies Attach to dependent containers
-t, --timeout TIMEOUT Use this timeout in seconds for container
shutdown when attached or when containers are
already running. (default: 10)
@@ -1034,18 +1066,16 @@ class TopLevelCommand(object):
remove_orphans = options['--remove-orphans']
detached = options.get('--detach')
no_start = options.get('--no-start')
attach_dependencies = options.get('--attach-dependencies')
if detached and (cascade_stop or exit_value_from or attach_dependencies):
raise UserError(
"-d cannot be combined with --abort-on-container-exit or --attach-dependencies.")
if detached and (cascade_stop or exit_value_from):
raise UserError("--abort-on-container-exit and -d cannot be combined.")
ignore_orphans = self.toplevel_environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
if ignore_orphans and remove_orphans:
raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.")
opts = ['--detach', '--abort-on-container-exit', '--exit-code-from', '--attach-dependencies']
opts = ['--detach', '--abort-on-container-exit', '--exit-code-from']
for excluded in [x for x in opts if options.get(x) and no_start]:
raise UserError('--no-start and {} cannot be combined.'.format(excluded))
@@ -1090,10 +1120,7 @@ class TopLevelCommand(object):
if detached or no_start:
return
attached_containers = filter_attached_containers(
to_attach,
service_names,
attach_dependencies)
attached_containers = filter_containers_to_service_names(to_attach, service_names)
log_printer = log_printer_from_project(
self.project,
@@ -1189,10 +1216,12 @@ def timeout_from_opts(options):
return None if timeout is None else int(timeout)
def image_digests_for_project(project):
def image_digests_for_project(project, allow_push=False):
try:
return get_image_digests(project)
return get_image_digests(
project,
allow_push=allow_push
)
except MissingDigests as e:
def list_images(images):
return "\n".join(" {}".format(name) for name in sorted(images))
@@ -1398,8 +1427,8 @@ def log_printer_from_project(
log_args=log_args)
def filter_attached_containers(containers, service_names, attach_dependencies=False):
if attach_dependencies or not service_names:
def filter_containers_to_service_names(containers, service_names):
if not service_names:
return containers
return [

View File

@@ -5,7 +5,6 @@ import functools
import io
import logging
import os
import re
import string
import sys
from collections import namedtuple
@@ -215,12 +214,6 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
.format(self.filename, VERSION_EXPLANATION)
)
version_pattern = re.compile(r"^[2-9]+(\.\d+)?$")
if not version_pattern.match(version):
raise ConfigurationError(
'Version "{}" in "{}" is invalid.'
.format(version, self.filename))
if version == '2':
return const.COMPOSEFILE_V2_0

View File

@@ -114,13 +114,3 @@ def get_digest_from_push(events):
if digest:
return digest
return None
def read_status(event):
status = event['status'].lower()
if 'progressDetail' in event:
detail = event['progressDetail']
if 'current' in detail and 'total' in detail:
percentage = float(detail['current']) / float(detail['total'])
status = '{} ({:.1%})'.format(status, percentage)
return status

View File

@@ -11,12 +11,9 @@ from os import path
import enum
import six
from docker.errors import APIError
from docker.errors import ImageNotFound
from docker.errors import NotFound
from docker.utils import version_lt
from . import parallel
from .cli.errors import UserError
from .config import ConfigurationError
from .config.config import V1
from .config.sort_services import get_container_name_from_network_mode
@@ -28,13 +25,11 @@ from .container import Container
from .network import build_networks
from .network import get_networks
from .network import ProjectNetworks
from .progress_stream import read_status
from .service import BuildAction
from .service import ContainerNetworkMode
from .service import ContainerPidMode
from .service import ConvergenceStrategy
from .service import NetworkMode
from .service import NoSuchImageError
from .service import parse_repository_tag
from .service import PidMode
from .service import Service
@@ -44,6 +39,7 @@ from .utils import microseconds_from_time_nano
from .utils import truncate_string
from .volume import ProjectVolumes
log = logging.getLogger(__name__)
@@ -87,11 +83,10 @@ class Project(object):
return labels
@classmethod
def from_config(cls, name, config_data, client, default_platform=None, extra_labels=None):
def from_config(cls, name, config_data, client, default_platform=None, extra_labels=[]):
"""
Construct a Project from a config.Config object.
"""
extra_labels = extra_labels or []
use_networking = (config_data.version and config_data.version != V1)
networks = build_networks(name, config_data, client)
project_networks = ProjectNetworks.from_services(
@@ -383,7 +378,6 @@ class Project(object):
def build_service(service):
service.build(no_cache, pull, force_rm, memory, build_args, gzip, rm, silent, cli, progress)
if parallel_build:
_, errors = parallel.parallel_execute(
services,
@@ -625,68 +619,49 @@ class Project(object):
def pull(self, service_names=None, ignore_pull_failures=False, parallel_pull=False, silent=False,
include_deps=False):
services = self.get_services(service_names, include_deps)
images_to_build = {service.image_name for service in services if service.can_be_built()}
services_to_pull = [service for service in services if service.image_name not in images_to_build]
msg = not silent and 'Pulling' or None
if parallel_pull:
self.parallel_pull(services, silent=silent)
def pull_service(service):
strm = service.pull(ignore_pull_failures, True, stream=True)
if strm is None: # Attempting to pull service with no `image` key is a no-op
return
else:
must_build = []
for service in services:
try:
service.pull(ignore_pull_failures, silent=silent)
except (ImageNotFound, NotFound):
if service.can_be_built():
must_build.append(service.name)
else:
raise
if len(must_build):
log.warning('Some service image(s) must be built from source by running:\n'
' docker-compose build {}'
.format(' '.join(must_build)))
def parallel_pull(self, services, ignore_pull_failures=False, silent=False):
msg = 'Pulling' if not silent else None
must_build = []
def pull_service(service):
strm = service.pull(ignore_pull_failures, True, stream=True)
if strm is None: # Attempting to pull service with no `image` key is a no-op
return
try:
writer = parallel.get_stream_writer()
for event in strm:
if 'status' not in event:
continue
status = read_status(event)
status = event['status'].lower()
if 'progressDetail' in event:
detail = event['progressDetail']
if 'current' in detail and 'total' in detail:
percentage = float(detail['current']) / float(detail['total'])
status = '{} ({:.1%})'.format(status, percentage)
writer.write(
msg, service.name, truncate_string(status), lambda s: s
)
except (ImageNotFound, NotFound):
if service.can_be_built():
must_build.append(service.name)
else:
raise
_, errors = parallel.parallel_execute(
services,
pull_service,
operator.attrgetter('name'),
msg,
limit=5,
)
_, errors = parallel.parallel_execute(
services_to_pull,
pull_service,
operator.attrgetter('name'),
msg,
limit=5,
)
if len(errors):
combined_errors = '\n'.join([
e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values()
])
raise ProjectError(combined_errors)
if len(must_build):
log.warning('Some service image(s) must be built from source by running:\n'
' docker-compose build {}'
.format(' '.join(must_build)))
if len(errors):
combined_errors = '\n'.join([
e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values()
])
raise ProjectError(combined_errors)
else:
for service in services_to_pull:
service.pull(ignore_pull_failures, silent=silent)
def push(self, service_names=None, ignore_push_failures=False):
unique_images = set()
@@ -847,91 +822,6 @@ def get_secrets(service, service_secrets, secret_defs):
return secrets
def get_image_digests(project):
digests = {}
needs_push = set()
needs_pull = set()
for service in project.services:
try:
digests[service.name] = get_image_digest(service)
except NeedsPush as e:
needs_push.add(e.image_name)
except NeedsPull as e:
needs_pull.add(e.service_name)
if needs_push or needs_pull:
raise MissingDigests(needs_push, needs_pull)
return digests
def get_image_digest(service):
if 'image' not in service.options:
raise UserError(
"Service '{s.name}' doesn't define an image tag. An image name is "
"required to generate a proper image digest. Specify an image repo "
"and tag with the 'image' option.".format(s=service))
_, _, separator = parse_repository_tag(service.options['image'])
# Compose file already uses a digest, no lookup required
if separator == '@':
return service.options['image']
digest = get_digest(service)
if digest:
return digest
if 'build' not in service.options:
raise NeedsPull(service.image_name, service.name)
raise NeedsPush(service.image_name)
def get_digest(service):
digest = None
try:
image = service.image()
# TODO: pick a digest based on the image tag if there are multiple
# digests
if image['RepoDigests']:
digest = image['RepoDigests'][0]
except NoSuchImageError:
try:
# Fetch the image digest from the registry
distribution = service.get_image_registry_data()
if distribution['Descriptor']['digest']:
digest = '{image_name}@{digest}'.format(
image_name=service.image_name,
digest=distribution['Descriptor']['digest']
)
except NoSuchImageError:
raise UserError(
"Digest not found for service '{service}'. "
"Repository does not exist or may require 'docker login'"
.format(service=service.name))
return digest
class MissingDigests(Exception):
def __init__(self, needs_push, needs_pull):
self.needs_push = needs_push
self.needs_pull = needs_pull
class NeedsPush(Exception):
def __init__(self, image_name):
self.image_name = image_name
class NeedsPull(Exception):
def __init__(self, image_name, service_name):
self.image_name = image_name
self.service_name = service_name
class NoSuchService(Exception):
def __init__(self, name):
if isinstance(name, six.binary_type):

View File

@@ -60,7 +60,6 @@ from .utils import parse_bytes
from .utils import parse_seconds_float
from .utils import truncate_id
from .utils import unique_everseen
from compose.cli.utils import binarystr_to_unicode
if six.PY2:
import subprocess32 as subprocess
@@ -185,7 +184,7 @@ class Service(object):
scale=1,
pid_mode=None,
default_platform=None,
extra_labels=None,
extra_labels=[],
**options
):
self.name = name
@@ -201,7 +200,7 @@ class Service(object):
self.scale_num = scale
self.default_platform = default_platform
self.options = options
self.extra_labels = extra_labels or []
self.extra_labels = extra_labels
def __repr__(self):
return '<Service: {}>'.format(self.name)
@@ -344,7 +343,7 @@ class Service(object):
return Container.create(self.client, **container_options)
except APIError as ex:
raise OperationFailedError("Cannot create container for service %s: %s" %
(self.name, binarystr_to_unicode(ex.explanation)))
(self.name, ex.explanation))
def ensure_image_exists(self, do_build=BuildAction.none, silent=False, cli=False):
if self.can_be_built() and do_build == BuildAction.force:
@@ -625,10 +624,9 @@ class Service(object):
try:
container.start()
except APIError as ex:
expl = binarystr_to_unicode(ex.explanation)
if "driver failed programming external connectivity" in expl:
if "driver failed programming external connectivity" in ex.explanation:
log.warn("Host is already in use by another container")
raise OperationFailedError("Cannot start service %s: %s" % (self.name, expl))
raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation))
return container
@property

View File

@@ -126,6 +126,18 @@ _docker_compose_build() {
}
_docker_compose_bundle() {
case "$prev" in
--output|-o)
_filedir
return
;;
esac
COMPREPLY=( $( compgen -W "--push-images --help --output -o" -- "$cur" ) )
}
_docker_compose_config() {
case "$prev" in
--hash)
@@ -545,7 +557,7 @@ _docker_compose_up() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--abort-on-container-exit --always-recreate-deps --attach-dependencies --build -d --detach --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-recreate --no-start --renew-anon-volumes -V --remove-orphans --scale --timeout -t" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--abort-on-container-exit --always-recreate-deps --build -d --detach --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-recreate --no-start --renew-anon-volumes -V --remove-orphans --scale --timeout -t" -- "$cur" ) )
;;
*)
__docker_compose_complete_services
@@ -569,6 +581,7 @@ _docker_compose() {
local commands=(
build
bundle
config
create
down

View File

@@ -121,6 +121,12 @@ __docker-compose_subcommand() {
'--parallel[Build images in parallel.]' \
'*:services:__docker-compose_services_from_build' && ret=0
;;
(bundle)
_arguments \
$opts_help \
'--push-images[Automatically push images for any services which have a `build` option specified.]' \
'(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to "<project name>.dab".]:file:_files' && ret=0
;;
(config)
_arguments \
$opts_help \
@@ -284,7 +290,7 @@ __docker-compose_subcommand() {
(up)
_arguments \
$opts_help \
'(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit and --attach-dependencies.]' \
'(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit.]' \
$opts_no_color \
$opts_no_deps \
$opts_force_recreate \
@@ -292,7 +298,6 @@ __docker-compose_subcommand() {
$opts_no_build \
"(--no-build)--build[Build images before starting containers.]" \
"(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \
"(-d)--attach-dependencies[Attach to dependent containers. Incompatible with -d.]" \
'(-t --timeout)'{-t,--timeout}"[Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)]:seconds: " \
'--scale[SERVICE=NUM Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.]:service scale SERVICE=NUM: ' \
'--exit-code-from=[Return the exit code of the selected service container. Implies --abort-on-container-exit]:service:__docker-compose_services' \

View File

@@ -1,108 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['bin/docker-compose'],
pathex=['.'],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='docker-compose',
debug=False,
strip=False,
upx=True,
console=True,
bootloader_ignore_signals=True)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
[
(
'compose/config/config_schema_v1.json',
'compose/config/config_schema_v1.json',
'DATA'
),
(
'compose/config/config_schema_v2.0.json',
'compose/config/config_schema_v2.0.json',
'DATA'
),
(
'compose/config/config_schema_v2.1.json',
'compose/config/config_schema_v2.1.json',
'DATA'
),
(
'compose/config/config_schema_v2.2.json',
'compose/config/config_schema_v2.2.json',
'DATA'
),
(
'compose/config/config_schema_v2.3.json',
'compose/config/config_schema_v2.3.json',
'DATA'
),
(
'compose/config/config_schema_v2.4.json',
'compose/config/config_schema_v2.4.json',
'DATA'
),
(
'compose/config/config_schema_v3.0.json',
'compose/config/config_schema_v3.0.json',
'DATA'
),
(
'compose/config/config_schema_v3.1.json',
'compose/config/config_schema_v3.1.json',
'DATA'
),
(
'compose/config/config_schema_v3.2.json',
'compose/config/config_schema_v3.2.json',
'DATA'
),
(
'compose/config/config_schema_v3.3.json',
'compose/config/config_schema_v3.3.json',
'DATA'
),
(
'compose/config/config_schema_v3.4.json',
'compose/config/config_schema_v3.4.json',
'DATA'
),
(
'compose/config/config_schema_v3.5.json',
'compose/config/config_schema_v3.5.json',
'DATA'
),
(
'compose/config/config_schema_v3.6.json',
'compose/config/config_schema_v3.6.json',
'DATA'
),
(
'compose/config/config_schema_v3.7.json',
'compose/config/config_schema_v3.7.json',
'DATA'
),
(
'compose/GITSHA',
'compose/GITSHA',
'DATA'
)
],
strip=False,
upx=True,
upx_exclude=[],
name='docker-compose-Darwin-x86_64')

View File

@@ -1 +1 @@
pyinstaller==3.6
pyinstaller==3.5

View File

@@ -1,7 +1,6 @@
coverage==5.0.3
ddt==1.2.2
flake8==3.7.9
coverage==4.4.2
ddt==1.2.0
flake8==3.5.0
mock==3.0.5
pytest==5.3.2; python_version >= '3.5'
pytest==4.6.5; python_version < '3.5'
pytest-cov==2.8.1
pytest==3.6.3
pytest-cov==2.5.1

View File

@@ -1,26 +1,25 @@
backports.shutil_get_terminal_size==1.0.0
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
cached-property==1.5.1
certifi==2019.11.28
cached-property==1.3.0
certifi==2017.4.17
chardet==3.0.4
colorama==0.4.3; sys_platform == 'win32'
colorama==0.4.0; sys_platform == 'win32'
docker==4.1.0
docker-pycreds==0.4.0
dockerpty==0.4.1
docopt==0.6.2
enum34==1.1.6; python_version < '3.4'
functools32==3.2.3.post2; python_version < '3.2'
idna==2.8
ipaddress==1.0.23
jsonschema==3.2.0
paramiko==2.7.1
idna==2.5
ipaddress==1.0.18
jsonschema==3.0.1
paramiko==2.6.0
pypiwin32==219; sys_platform == 'win32' and python_version < '3.6'
pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6'
PySocks==1.7.1
PyYAML==5.3
PySocks==1.6.7
PyYAML==4.2b1
requests==2.22.0
six==1.12.0
subprocess32==3.5.4; python_version < '3.2'
texttable==1.6.2
urllib3==1.25.7; python_version == '3.3'
websocket-client==0.57.0
urllib3==1.24.2; python_version == '3.3'
websocket-client==0.32.0

20
script/Jenkinsfile.fossa Normal file
View File

@@ -0,0 +1,20 @@
pipeline {
agent any
stages {
stage("License Scan") {
agent {
label 'ubuntu-1604-aufs-edge'
}
steps {
withCredentials([
string(credentialsId: 'fossa-api-key', variable: 'FOSSA_API_KEY')
]) {
checkout scm
sh "FOSSA_API_KEY='${FOSSA_API_KEY}' BRANCH_NAME='${env.BRANCH_NAME}' make -f script/fossa.mk fossa-analyze"
sh "FOSSA_API_KEY='${FOSSA_API_KEY}' make -f script/fossa.mk fossa-test"
}
}
}
}
}

View File

@@ -24,7 +24,7 @@ if [ ! -z "${BUILD_BOOTLOADER}" ]; then
git clone --single-branch --branch develop https://github.com/pyinstaller/pyinstaller.git /tmp/pyinstaller
cd /tmp/pyinstaller/bootloader
# Checkout commit corresponding to version in requirements-build
git checkout v3.6
git checkout v3.5
"${VENV}"/bin/python3 ./waf configure --no-lsb all
"${VENV}"/bin/pip3 install ..
cd "${CODE_PATH}"

View File

@@ -11,14 +11,6 @@ venv/bin/pip install -r requirements-build.txt
venv/bin/pip install --no-deps .
DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA
# Build as a folder for macOS Catalina.
venv/bin/pyinstaller docker-compose_darwin.spec
dist/docker-compose-Darwin-x86_64/docker-compose version
(cd dist/docker-compose-Darwin-x86_64/ && tar zcvf ../docker-compose-Darwin-x86_64.tgz .)
rm -rf dist/docker-compose-Darwin-x86_64
# Build static binary for legacy.
venv/bin/pyinstaller docker-compose.spec
mv dist/docker-compose dist/docker-compose-Darwin-x86_64
dist/docker-compose-Darwin-x86_64 version

View File

@@ -25,11 +25,3 @@ curl -f -T dist/docker-compose-${OS_NAME}-x86_64 -u$BINTRAY_USERNAME:$BINTRAY_AP
-H "X-Bintray-Package: ${PKG_NAME}" -H "X-Bintray-Version: $CIRCLE_BRANCH" \
-H "X-Bintray-Override: 1" -H "X-Bintray-Publish: 1" -X PUT \
https://api.bintray.com/content/docker-compose/${CIRCLE_BRANCH}/docker-compose-${OS_NAME}-x86_64 || exit 1
# Upload folder format of docker-compose for macOS in addition to binary.
if [ "${OS_NAME}" == "Darwin" ]; then
curl -f -T dist/docker-compose-${OS_NAME}-x86_64.tgz -u$BINTRAY_USERNAME:$BINTRAY_API_KEY \
-H "X-Bintray-Package: ${PKG_NAME}" -H "X-Bintray-Version: $CIRCLE_BRANCH" \
-H "X-Bintray-Override: 1" -H "X-Bintray-Publish: 1" -X PUT \
https://api.bintray.com/content/docker-compose/${CIRCLE_BRANCH}/docker-compose-${OS_NAME}-x86_64.tgz || exit 1
fi

16
script/fossa.mk Normal file
View File

@@ -0,0 +1,16 @@
# Variables for Fossa
BUILD_ANALYZER?=docker/fossa-analyzer
FOSSA_OPTS?=--option all-tags:true --option allow-unresolved:true
fossa-analyze:
docker run --rm -e FOSSA_API_KEY=$(FOSSA_API_KEY) \
-v $(CURDIR)/$*:/go/src/github.com/docker/compose \
-w /go/src/github.com/docker/compose \
$(BUILD_ANALYZER) analyze ${FOSSA_OPTS} --branch ${BRANCH_NAME}
# This command is used to run the fossa test command
fossa-test:
docker run -i -e FOSSA_API_KEY=$(FOSSA_API_KEY) \
-v $(CURDIR)/$*:/go/src/github.com/docker/compose \
-w /go/src/github.com/docker/compose \
$(BUILD_ANALYZER) test

View File

@@ -1,39 +0,0 @@
#!/bin/bash
set -e
set -x
## Usage :
## changelog PREVIOUS_TAG..HEAD
# configure refs so we get pull-requests metadata
git config --add remote.origin.fetch +refs/pull/*/head:refs/remotes/origin/pull/*
git fetch origin
RANGE=${1:-"$(git describe --tags --abbrev=0)..HEAD"}
echo "Generate changelog for range ${RANGE}"
echo
pullrequests() {
for commit in $(git log ${RANGE} --format='format:%H'); do
# Get the oldest remotes/origin/pull/* branch to include this commit, i.e. the one to introduce it
git branch -a --sort=committerdate --contains $commit --list 'origin/pull/*' | head -1 | cut -d'/' -f4
done
}
changes=$(pullrequests | uniq)
echo "pull requests merged within range:"
echo $changes
echo '#Features' > CHANGELOG.md
for pr in $changes; do
curl -fs -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/docker/compose/pulls/${pr} \
| jq -r ' select( .labels[].name | contains("kind/feature") ) | "* "+.title' >> CHANGELOG.md
done
echo '#Bugs' >> CHANGELOG.md
for pr in $changes; do
curl -fs -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/docker/compose/pulls/${pr} \
| jq -r ' select( .labels[].name | contains("kind/bug") ) | "* "+.title' >> CHANGELOG.md
done

View File

@@ -204,8 +204,7 @@ def resume(args):
gh_release = create_release_draft(repository, args.release, pr_data, files)
delete_assets(gh_release)
upload_assets(gh_release, files)
tag_as_latest = is_tag_latest(args.release)
img_manager = ImageManager(args.release, tag_as_latest)
img_manager = ImageManager(args.release)
img_manager.build_images(repository)
except ScriptError as e:
print(e)
@@ -245,8 +244,7 @@ def start(args):
files = downloader.download_all(args.release)
gh_release = create_release_draft(repository, args.release, pr_data, files)
upload_assets(gh_release, files)
tag_as_latest = is_tag_latest(args.release)
img_manager = ImageManager(args.release, tag_as_latest)
img_manager = ImageManager(args.release)
img_manager.build_images(repository)
except ScriptError as e:
print(e)

View File

@@ -55,7 +55,6 @@ class BinaryDownloader(requests.Session):
def download_all(self, version):
files = {
'docker-compose-Darwin-x86_64.tgz': None,
'docker-compose-Darwin-x86_64': None,
'docker-compose-Linux-x86_64': None,
'docker-compose-Windows-x86_64.exe': None,

View File

@@ -15,7 +15,7 @@
set -e
VERSION="1.25.2-rc1"
VERSION="1.24.0"
IMAGE="docker/compose:$VERSION"
@@ -36,18 +36,18 @@ if [ "$(pwd)" != '/' ]; then
fi
if [ -n "$COMPOSE_FILE" ]; then
COMPOSE_OPTIONS="$COMPOSE_OPTIONS -e COMPOSE_FILE=$COMPOSE_FILE"
compose_dir=$(realpath "$(dirname "$COMPOSE_FILE")")
compose_dir=$(realpath $(dirname $COMPOSE_FILE))
fi
# TODO: also check --file argument
if [ -n "$compose_dir" ]; then
VOLUMES="$VOLUMES -v $compose_dir:$compose_dir"
fi
if [ -n "$HOME" ]; then
VOLUMES="$VOLUMES -v $HOME:$HOME -e HOME" # Pass in HOME to share docker.config and allow ~/-relative paths to work.
VOLUMES="$VOLUMES -v $HOME:$HOME -v $HOME:/root" # mount $HOME in /root to share docker.config
fi
# Only allocate tty if we detect one
if [ -t 0 ] && [ -t 1 ]; then
if [ -t 0 -a -t 1 ]; then
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -t"
fi
@@ -56,9 +56,8 @@ DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"
# Handle userns security
if docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'name=userns'; then
if [ ! -z "$(docker info 2>/dev/null | grep userns)" ]; then
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS --userns=host"
fi
# shellcheck disable=SC2086
exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@"

View File

@@ -13,13 +13,13 @@ if ! [ ${DEPLOYMENT_TARGET} == "$(macos_version)" ]; then
SDK_SHA1=dd228a335194e3392f1904ce49aff1b1da26ca62
fi
OPENSSL_VERSION=1.1.1d
OPENSSL_VERSION=1.1.1c
OPENSSL_URL=https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
OPENSSL_SHA1=056057782325134b76d1931c48f2c7e6595d7ef4
OPENSSL_SHA1=71b830a077276cbeccc994369538617a21bee808
PYTHON_VERSION=3.7.5
PYTHON_VERSION=3.7.4
PYTHON_URL=https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz
PYTHON_SHA1=8b0311d4cca19f0ea9181731189fa33c9f5aedf9
PYTHON_SHA1=fb1d764be8a9dcd40f2f152a610a0ab04e0d0ed3
#
# Install prerequisites.
@@ -36,7 +36,7 @@ if ! [ -x "$(command -v python3)" ]; then
brew install python3
fi
if ! [ -x "$(command -v virtualenv)" ]; then
pip3 install virtualenv==16.2.0
pip install virtualenv==16.2.0
fi
#

View File

@@ -32,7 +32,7 @@ def find_version(*file_paths):
install_requires = [
'cached-property >= 1.2.0, < 2',
'docopt >= 0.6.1, < 1',
'PyYAML >= 3.10, < 6',
'PyYAML >= 3.10, < 5',
'requests >= 2.20.0, < 3',
'texttable >= 0.9.0, < 2',
'websocket-client >= 0.32.0, < 1',

View File

@@ -43,30 +43,11 @@ ProcessResult = namedtuple('ProcessResult', 'stdout stderr')
BUILD_CACHE_TEXT = 'Using cache'
BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:1.27.2'
COMPOSE_COMPATIBILITY_DICT = {
'version': '2.3',
'volumes': {'foo': {'driver': 'default'}},
'networks': {'bar': {}},
'services': {
'foo': {
'command': '/bin/true',
'image': 'alpine:3.10.1',
'scale': 3,
'restart': 'always:7',
'mem_limit': '300M',
'mem_reservation': '100M',
'cpus': 0.7,
'volumes': ['foo:/bar:rw'],
'networks': {'bar': None},
}
},
}
def start_process(base_dir, options):
proc = subprocess.Popen(
['docker-compose'] + options,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=base_dir)
@@ -74,8 +55,8 @@ def start_process(base_dir, options):
return proc
def wait_on_process(proc, returncode=0, stdin=None):
stdout, stderr = proc.communicate(input=stdin)
def wait_on_process(proc, returncode=0):
stdout, stderr = proc.communicate()
if proc.returncode != returncode:
print("Stderr: {}".format(stderr))
print("Stdout: {}".format(stdout))
@@ -83,10 +64,10 @@ def wait_on_process(proc, returncode=0, stdin=None):
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
def dispatch(base_dir, options, project_options=None, returncode=0, stdin=None):
def dispatch(base_dir, options, project_options=None, returncode=0):
project_options = project_options or []
proc = start_process(base_dir, project_options + options)
return wait_on_process(proc, returncode=returncode, stdin=stdin)
return wait_on_process(proc, returncode=returncode)
def wait_on_condition(condition, delay=0.1, timeout=40):
@@ -175,8 +156,8 @@ class CLITestCase(DockerClientTestCase):
self._project = get_project(self.base_dir, override_dir=self.override_dir)
return self._project
def dispatch(self, options, project_options=None, returncode=0, stdin=None):
return dispatch(self.base_dir, options, project_options, returncode, stdin)
def dispatch(self, options, project_options=None, returncode=0):
return dispatch(self.base_dir, options, project_options, returncode)
def execute(self, container, cmd):
# Remove once Hijack and CloseNotifier sign a peace treaty
@@ -260,17 +241,6 @@ class CLITestCase(DockerClientTestCase):
self.base_dir = 'tests/fixtures/v2-full'
assert self.dispatch(['config', '--quiet']).stdout == ''
def test_config_stdin(self):
config = b"""version: "3.7"
services:
web:
image: nginx
other:
image: alpine
"""
result = self.dispatch(['-f', '-', 'config', '--services'], stdin=config)
assert set(result.stdout.rstrip().split('\n')) == {'web', 'other'}
def test_config_with_hash_option(self):
self.base_dir = 'tests/fixtures/v2-full'
result = self.dispatch(['config', '--hash=*'])
@@ -287,7 +257,7 @@ services:
# assert there are no python objects encoded in the output
assert '!!' not in result.stdout
output = yaml.safe_load(result.stdout)
output = yaml.load(result.stdout)
expected = {
'version': '2.0',
'volumes': {'data': {'driver': 'local'}},
@@ -312,7 +282,7 @@ services:
def test_config_restart(self):
self.base_dir = 'tests/fixtures/restart'
result = self.dispatch(['config'])
assert yaml.safe_load(result.stdout) == {
assert yaml.load(result.stdout) == {
'version': '2.0',
'services': {
'never': {
@@ -341,7 +311,7 @@ services:
def test_config_external_network(self):
self.base_dir = 'tests/fixtures/networks'
result = self.dispatch(['-f', 'external-networks.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'networks' in json_result
assert json_result['networks'] == {
'networks_foo': {
@@ -355,7 +325,7 @@ services:
def test_config_with_dot_env(self):
self.base_dir = 'tests/fixtures/default-env-file'
result = self.dispatch(['config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert json_result == {
'services': {
'web': {
@@ -370,7 +340,7 @@ services:
def test_config_with_env_file(self):
self.base_dir = 'tests/fixtures/default-env-file'
result = self.dispatch(['--env-file', '.env2', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert json_result == {
'services': {
'web': {
@@ -385,7 +355,7 @@ services:
def test_config_with_dot_env_and_override_dir(self):
self.base_dir = 'tests/fixtures/default-env-file'
result = self.dispatch(['--project-directory', 'alt/', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert json_result == {
'services': {
'web': {
@@ -400,7 +370,7 @@ services:
def test_config_external_volume_v2(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes-v2.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
@@ -416,7 +386,7 @@ services:
def test_config_external_volume_v2_x(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes-v2-x.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
@@ -432,7 +402,7 @@ services:
def test_config_external_volume_v3_x(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes-v3-x.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
@@ -448,7 +418,7 @@ services:
def test_config_external_volume_v3_4(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes-v3-4.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
@@ -464,7 +434,7 @@ services:
def test_config_external_network_v3_5(self):
self.base_dir = 'tests/fixtures/networks'
result = self.dispatch(['-f', 'external-networks-v3-5.yml', 'config'])
json_result = yaml.safe_load(result.stdout)
json_result = yaml.load(result.stdout)
assert 'networks' in json_result
assert json_result['networks'] == {
'foo': {
@@ -480,7 +450,7 @@ services:
def test_config_v1(self):
self.base_dir = 'tests/fixtures/v1-config'
result = self.dispatch(['config'])
assert yaml.safe_load(result.stdout) == {
assert yaml.load(result.stdout) == {
'version': '2.1',
'services': {
'net': {
@@ -505,7 +475,7 @@ services:
self.base_dir = 'tests/fixtures/v3-full'
result = self.dispatch(['config'])
assert yaml.safe_load(result.stdout) == {
assert yaml.load(result.stdout) == {
'version': '3.5',
'volumes': {
'foobar': {
@@ -582,23 +552,24 @@ services:
self.base_dir = 'tests/fixtures/compatibility-mode'
result = self.dispatch(['--compatibility', 'config'])
assert yaml.load(result.stdout) == COMPOSE_COMPATIBILITY_DICT
@mock.patch.dict(os.environ)
def test_config_compatibility_mode_from_env(self):
self.base_dir = 'tests/fixtures/compatibility-mode'
os.environ['COMPOSE_COMPATIBILITY'] = 'true'
result = self.dispatch(['config'])
assert yaml.load(result.stdout) == COMPOSE_COMPATIBILITY_DICT
@mock.patch.dict(os.environ)
def test_config_compatibility_mode_from_env_and_option_precedence(self):
self.base_dir = 'tests/fixtures/compatibility-mode'
os.environ['COMPOSE_COMPATIBILITY'] = 'false'
result = self.dispatch(['--compatibility', 'config'])
assert yaml.load(result.stdout) == COMPOSE_COMPATIBILITY_DICT
assert yaml.load(result.stdout) == {
'version': '2.3',
'volumes': {'foo': {'driver': 'default'}},
'networks': {'bar': {}},
'services': {
'foo': {
'command': '/bin/true',
'image': 'alpine:3.10.1',
'scale': 3,
'restart': 'always:7',
'mem_limit': '300M',
'mem_reservation': '100M',
'cpus': 0.7,
'volumes': ['foo:/bar:rw'],
'networks': {'bar': None},
}
},
}
def test_ps(self):
self.project.get_service('simple').create_container()
@@ -690,6 +661,13 @@ services:
'image library/nonexisting-image:latest not found' in result.stderr or
'pull access denied for nonexisting-image' in result.stderr)
def test_pull_with_build(self):
result = self.dispatch(['-f', 'pull-with-build.yml', 'pull'])
assert 'Pulling simple' not in result.stderr
assert 'Pulling from_simple' not in result.stderr
assert 'Pulling another ...' in result.stderr
def test_pull_with_quiet(self):
assert self.dispatch(['pull', '--quiet']).stderr == ''
assert self.dispatch(['pull', '--quiet']).stdout == ''
@@ -711,14 +689,6 @@ services:
result.stderr
)
def test_pull_can_build(self):
result = self.dispatch([
'-f', 'can-build-pull-failures.yml', 'pull'],
returncode=0
)
assert 'Some service image(s) must be built from source' in result.stderr
assert 'docker-compose build can_build' in result.stderr
def test_pull_with_no_deps(self):
self.base_dir = 'tests/fixtures/links-composefile'
result = self.dispatch(['pull', '--no-parallel', 'web'])
@@ -872,6 +842,32 @@ services:
)
assert 'Favorite Touhou Character: hong.meiling' in result.stdout
def test_bundle_with_digests(self):
self.base_dir = 'tests/fixtures/bundle-with-digests/'
tmpdir = pytest.ensuretemp('cli_test_bundle')
self.addCleanup(tmpdir.remove)
filename = str(tmpdir.join('example.dab'))
self.dispatch(['bundle', '--output', filename])
with open(filename, 'r') as fh:
bundle = json.load(fh)
assert bundle == {
'Version': '0.1',
'Services': {
'web': {
'Image': ('dockercloud/hello-world@sha256:fe79a2cfbd17eefc3'
'44fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d'),
'Networks': ['default'],
},
'redis': {
'Image': ('redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d'
'374b2b7392de1e7d77be26ef8f7b'),
'Networks': ['default'],
}
},
}
def test_build_override_dir(self):
self.base_dir = 'tests/fixtures/build-path-override-dir'
self.override_dir = os.path.abspath('tests/fixtures')
@@ -1571,26 +1567,6 @@ services:
assert len(db.containers()) == 0
assert len(console.containers()) == 0
def test_up_with_attach_dependencies(self):
self.base_dir = 'tests/fixtures/echo-services-dependencies'
result = self.dispatch(['up', '--attach-dependencies', '--no-color', 'simple'], None)
simple_name = self.project.get_service('simple').containers(stopped=True)[0].name_without_project
another_name = self.project.get_service('another').containers(
stopped=True
)[0].name_without_project
assert '{} | simple'.format(simple_name) in result.stdout
assert '{} | another'.format(another_name) in result.stdout
def test_up_handles_aborted_dependencies(self):
self.base_dir = 'tests/fixtures/abort-on-container-exit-dependencies'
proc = start_process(
self.base_dir,
['up', 'simple', '--attach-dependencies', '--abort-on-container-exit'])
wait_on_condition(ContainerCountCondition(self.project, 0))
proc.wait()
assert proc.returncode == 1
def test_up_with_force_recreate(self):
self.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')

View File

@@ -1,10 +0,0 @@
version: "2.0"
services:
simple:
image: busybox:1.31.0-uclibc
command: top
depends_on:
- another
another:
image: busybox:1.31.0-uclibc
command: ls /thecakeisalie

View File

@@ -0,0 +1,9 @@
version: '2.0'
services:
web:
image: dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d
redis:
image: redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d374b2b7392de1e7d77be26ef8f7b

View File

@@ -1,10 +0,0 @@
version: "2.0"
services:
simple:
image: busybox:1.31.0-uclibc
command: echo simple
depends_on:
- another
another:
image: busybox:1.31.0-uclibc
command: echo another

View File

@@ -1,6 +0,0 @@
version: '3'
services:
can_build:
image: nonexisting-image-but-can-build:latest
build: .
command: top

View File

@@ -1,7 +1,6 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import contextlib
import os
from compose.config.config import ConfigDetails
@@ -56,17 +55,3 @@ def create_host_file(client, filename):
content = fh.read()
return create_custom_host_file(client, filename, content)
@contextlib.contextmanager
def cd(path):
"""
A context manager which changes the working directory to the given
path, and then changes it back to its previous value on exit.
"""
prev_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(prev_cwd)

View File

@@ -8,6 +8,7 @@ import random
import shutil
import tempfile
import py
import pytest
from docker.errors import APIError
from docker.errors import NotFound
@@ -15,7 +16,6 @@ from docker.errors import NotFound
from .. import mock
from ..helpers import build_config as load_config
from ..helpers import BUSYBOX_IMAGE_WITH_TAG
from ..helpers import cd
from ..helpers import create_host_file
from .testcases import DockerClientTestCase
from .testcases import SWARM_SKIP_CONTAINERS_ALL
@@ -1329,9 +1329,9 @@ class ProjectTest(DockerClientTestCase):
})
details = config.ConfigDetails('.', [base_file, override_file])
tmpdir = tempfile.mkdtemp('logging_test')
self.addCleanup(shutil.rmtree, tmpdir)
with cd(tmpdir):
tmpdir = py.test.ensuretemp('logging_test')
self.addCleanup(tmpdir.remove)
with tmpdir.as_cwd():
config_data = config.load(details)
project = Project.from_config(
name='composetest', config_data=config_data, client=self.client

View File

@@ -6,10 +6,8 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import copy
import os
import shutil
import tempfile
import py
from docker.errors import ImageNotFound
from ..helpers import BUSYBOX_IMAGE_WITH_TAG
@@ -428,32 +426,29 @@ class ServiceStateTest(DockerClientTestCase):
@no_cluster('Can not guarantee the build will be run on the same node the service is deployed')
def test_trigger_recreate_with_build(self):
context = tempfile.mkdtemp('test_trigger_recreate_with_build')
self.addCleanup(shutil.rmtree, context)
context = py.test.ensuretemp('test_trigger_recreate_with_build')
self.addCleanup(context.remove)
base_image = "FROM busybox\nLABEL com.docker.compose.test_image=true\n"
dockerfile = os.path.join(context, 'Dockerfile')
with open(dockerfile, mode="w") as dockerfile_fh:
dockerfile_fh.write(base_image)
dockerfile = context.join('Dockerfile')
dockerfile.write(base_image)
web = self.create_service('web', build={'context': str(context)})
container = web.create_container()
with open(dockerfile, mode="w") as dockerfile_fh:
dockerfile_fh.write(base_image + 'CMD echo hello world\n')
dockerfile.write(base_image + 'CMD echo hello world\n')
web.build()
web = self.create_service('web', build={'context': str(context)})
assert ('recreate', [container]) == web.convergence_plan()
def test_image_changed_to_build(self):
context = tempfile.mkdtemp('test_image_changed_to_build')
self.addCleanup(shutil.rmtree, context)
with open(os.path.join(context, 'Dockerfile'), mode="w") as dockerfile:
dockerfile.write("""
FROM busybox
LABEL com.docker.compose.test_image=true
""")
context = py.test.ensuretemp('test_image_changed_to_build')
self.addCleanup(context.remove)
context.join('Dockerfile').write("""
FROM busybox
LABEL com.docker.compose.test_image=true
""")
web = self.create_service('web', image='busybox')
container = web.create_container()

233
tests/unit/bundle_test.py Normal file
View File

@@ -0,0 +1,233 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import docker
import pytest
from .. import mock
from compose import bundle
from compose import service
from compose.cli.errors import UserError
from compose.config.config import Config
from compose.const import COMPOSEFILE_V2_0 as V2_0
from compose.service import NoSuchImageError
@pytest.fixture
def mock_service():
return mock.create_autospec(
service.Service,
client=mock.create_autospec(docker.APIClient),
options={})
def test_get_image_digest_exists(mock_service):
mock_service.options['image'] = 'abcd'
mock_service.image.return_value = {'RepoDigests': ['digest1']}
digest = bundle.get_image_digest(mock_service)
assert digest == 'digest1'
def test_get_image_digest_image_uses_digest(mock_service):
mock_service.options['image'] = image_id = 'redis@sha256:digest'
digest = bundle.get_image_digest(mock_service)
assert digest == image_id
assert not mock_service.image.called
def test_get_image_digest_from_repository(mock_service):
mock_service.options['image'] = 'abcd'
mock_service.image_name = 'abcd'
mock_service.image.side_effect = NoSuchImageError(None)
mock_service.get_image_registry_data.return_value = {'Descriptor': {'digest': 'digest'}}
digest = bundle.get_image_digest(mock_service)
assert digest == 'abcd@digest'
def test_get_image_digest_no_image(mock_service):
with pytest.raises(UserError) as exc:
bundle.get_image_digest(service.Service(name='theservice'))
assert "doesn't define an image tag" in exc.exconly()
def test_push_image_with_saved_digest(mock_service):
mock_service.options['build'] = '.'
mock_service.options['image'] = image_id = 'abcd'
mock_service.push.return_value = expected = 'sha256:thedigest'
mock_service.image.return_value = {'RepoDigests': ['digest1']}
digest = bundle.push_image(mock_service)
assert digest == image_id + '@' + expected
mock_service.push.assert_called_once_with()
assert not mock_service.client.push.called
def test_push_image(mock_service):
mock_service.options['build'] = '.'
mock_service.options['image'] = image_id = 'abcd'
mock_service.push.return_value = expected = 'sha256:thedigest'
mock_service.image.return_value = {'RepoDigests': []}
digest = bundle.push_image(mock_service)
assert digest == image_id + '@' + expected
mock_service.push.assert_called_once_with()
mock_service.client.pull.assert_called_once_with(digest)
def test_to_bundle():
image_digests = {'a': 'aaaa', 'b': 'bbbb'}
services = [
{'name': 'a', 'build': '.', },
{'name': 'b', 'build': './b'},
]
config = Config(
version=V2_0,
services=services,
volumes={'special': {}},
networks={'extra': {}},
secrets={},
configs={}
)
with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log:
output = bundle.to_bundle(config, image_digests)
assert mock_log.mock_calls == [
mock.call("Unsupported top level key 'networks' - ignoring"),
mock.call("Unsupported top level key 'volumes' - ignoring"),
]
assert output == {
'Version': '0.1',
'Services': {
'a': {'Image': 'aaaa', 'Networks': ['default']},
'b': {'Image': 'bbbb', 'Networks': ['default']},
}
}
def test_convert_service_to_bundle():
name = 'theservice'
image_digest = 'thedigest'
service_dict = {
'ports': ['80'],
'expose': ['1234'],
'networks': {'extra': {}},
'command': 'foo',
'entrypoint': 'entry',
'environment': {'BAZ': 'ENV'},
'build': '.',
'working_dir': '/tmp',
'user': 'root',
'labels': {'FOO': 'LABEL'},
'privileged': True,
}
with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log:
config = bundle.convert_service_to_bundle(name, service_dict, image_digest)
mock_log.assert_called_once_with(
"Unsupported key 'privileged' in services.theservice - ignoring")
assert config == {
'Image': image_digest,
'Ports': [
{'Protocol': 'tcp', 'Port': 80},
{'Protocol': 'tcp', 'Port': 1234},
],
'Networks': ['extra'],
'Command': ['entry', 'foo'],
'Env': ['BAZ=ENV'],
'WorkingDir': '/tmp',
'User': 'root',
'Labels': {'FOO': 'LABEL'},
}
def test_set_command_and_args_none():
config = {}
bundle.set_command_and_args(config, [], [])
assert config == {}
def test_set_command_and_args_from_command():
config = {}
bundle.set_command_and_args(config, [], "echo ok")
assert config == {'Args': ['echo', 'ok']}
def test_set_command_and_args_from_entrypoint():
config = {}
bundle.set_command_and_args(config, "echo entry", [])
assert config == {'Command': ['echo', 'entry']}
def test_set_command_and_args_from_both():
config = {}
bundle.set_command_and_args(config, "echo entry", ["extra", "arg"])
assert config == {'Command': ['echo', 'entry', "extra", "arg"]}
def test_make_service_networks_default():
name = 'theservice'
service_dict = {}
with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log:
networks = bundle.make_service_networks(name, service_dict)
assert not mock_log.called
assert networks == ['default']
def test_make_service_networks():
name = 'theservice'
service_dict = {
'networks': {
'foo': {
'aliases': ['one', 'two'],
},
'bar': {}
},
}
with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log:
networks = bundle.make_service_networks(name, service_dict)
mock_log.assert_called_once_with(
"Unsupported key 'aliases' in services.theservice.networks.foo - ignoring")
assert sorted(networks) == sorted(service_dict['networks'])
def test_make_port_specs():
service_dict = {
'expose': ['80', '500/udp'],
'ports': [
'400:80',
'222',
'127.0.0.1:8001:8001',
'127.0.0.1:5000-5001:3000-3001'],
}
port_specs = bundle.make_port_specs(service_dict)
assert port_specs == [
{'Protocol': 'tcp', 'Port': 80},
{'Protocol': 'tcp', 'Port': 222},
{'Protocol': 'tcp', 'Port': 8001},
{'Protocol': 'tcp', 'Port': 3000},
{'Protocol': 'tcp', 'Port': 3001},
{'Protocol': 'udp', 'Port': 500},
]
def test_make_port_spec_with_protocol():
port_spec = bundle.make_port_spec("5000/udp")
assert port_spec == {'Protocol': 'udp', 'Port': 5000}
def test_make_port_spec_default_protocol():
port_spec = bundle.make_port_spec("50000")
assert port_spec == {'Protocol': 'tcp', 'Port': 50000}

View File

@@ -12,7 +12,7 @@ from compose.cli.formatter import ConsoleWarningFormatter
from compose.cli.main import build_one_off_container_options
from compose.cli.main import call_docker
from compose.cli.main import convergence_strategy_from_opts
from compose.cli.main import filter_attached_containers
from compose.cli.main import filter_containers_to_service_names
from compose.cli.main import get_docker_start_call
from compose.cli.main import setup_console_handler
from compose.cli.main import warn_for_swarm_mode
@@ -37,7 +37,7 @@ def logging_handler():
class TestCLIMainTestCase(object):
def test_filter_attached_containers(self):
def test_filter_containers_to_service_names(self):
containers = [
mock_container('web', 1),
mock_container('web', 2),
@@ -46,29 +46,17 @@ class TestCLIMainTestCase(object):
mock_container('another', 1),
]
service_names = ['web', 'db']
actual = filter_attached_containers(containers, service_names)
actual = filter_containers_to_service_names(containers, service_names)
assert actual == containers[:3]
def test_filter_attached_containers_with_dependencies(self):
containers = [
mock_container('web', 1),
mock_container('web', 2),
mock_container('db', 1),
mock_container('other', 1),
mock_container('another', 1),
]
service_names = ['web', 'db']
actual = filter_attached_containers(containers, service_names, attach_dependencies=True)
assert actual == containers
def test_filter_attached_containers_all(self):
def test_filter_containers_to_service_names_all(self):
containers = [
mock_container('web', 1),
mock_container('db', 1),
mock_container('other', 1),
]
service_names = []
actual = filter_attached_containers(containers, service_names)
actual = filter_containers_to_service_names(containers, service_names)
assert actual == containers
def test_warning_in_swarm_mode(self):

View File

@@ -10,14 +10,12 @@ import tempfile
from operator import itemgetter
from random import shuffle
import py
import pytest
import yaml
from ddt import data
from ddt import ddt
from ...helpers import build_config_details
from ...helpers import BUSYBOX_IMAGE_WITH_TAG
from ...helpers import cd
from compose.config import config
from compose.config import types
from compose.config.config import ConfigFile
@@ -70,7 +68,6 @@ def secret_sort(secrets):
return sorted(secrets, key=itemgetter('source'))
@ddt
class ConfigTest(unittest.TestCase):
def test_load(self):
@@ -780,14 +777,13 @@ class ConfigTest(unittest.TestCase):
})
details = config.ConfigDetails('.', [base_file, override_file])
tmpdir = tempfile.mkdtemp('config_test')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'common.yml'), mode="w") as common_fh:
common_fh.write("""
base:
labels: ['label=one']
""")
with cd(tmpdir):
tmpdir = py.test.ensuretemp('config_test')
self.addCleanup(tmpdir.remove)
tmpdir.join('common.yml').write("""
base:
labels: ['label=one']
""")
with tmpdir.as_cwd():
service_dicts = config.load(details).services
expected = [
@@ -816,20 +812,19 @@ class ConfigTest(unittest.TestCase):
}
)
tmpdir = tempfile.mkdtemp('config_test')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'base.yml'), mode="w") as base_fh:
base_fh.write("""
version: '2.2'
services:
base:
image: base
web:
extends: base
""")
tmpdir = pytest.ensuretemp('config_test')
self.addCleanup(tmpdir.remove)
tmpdir.join('base.yml').write("""
version: '2.2'
services:
base:
image: base
web:
extends: base
""")
details = config.ConfigDetails('.', [main_file])
with cd(tmpdir):
with tmpdir.as_cwd():
service_dicts = config.load(details).services
assert service_dicts[0] == {
'name': 'prodweb',
@@ -1767,23 +1762,22 @@ class ConfigTest(unittest.TestCase):
assert services[0]['environment']['SPRING_JPA_HIBERNATE_DDL-AUTO'] == 'none'
def test_load_yaml_with_yaml_error(self):
tmpdir = tempfile.mkdtemp('invalid_yaml_test')
self.addCleanup(shutil.rmtree, tmpdir)
invalid_yaml_file = os.path.join(tmpdir, 'docker-compose.yml')
with open(invalid_yaml_file, mode="w") as invalid_yaml_file_fh:
invalid_yaml_file_fh.write("""
web:
this is bogus: ok: what
""")
tmpdir = py.test.ensuretemp('invalid_yaml_test')
self.addCleanup(tmpdir.remove)
invalid_yaml_file = tmpdir.join('docker-compose.yml')
invalid_yaml_file.write("""
web:
this is bogus: ok: what
""")
with pytest.raises(ConfigurationError) as exc:
config.load_yaml(str(invalid_yaml_file))
assert 'line 3, column 22' in exc.exconly()
assert 'line 3, column 32' in exc.exconly()
def test_load_yaml_with_bom(self):
tmpdir = tempfile.mkdtemp('bom_yaml')
self.addCleanup(shutil.rmtree, tmpdir)
bom_yaml = os.path.join(tmpdir, 'docker-compose.yml')
tmpdir = py.test.ensuretemp('bom_yaml')
self.addCleanup(tmpdir.remove)
bom_yaml = tmpdir.join('docker-compose.yml')
with codecs.open(str(bom_yaml), 'w', encoding='utf-8') as f:
f.write('''\ufeff
version: '2.3'
@@ -1891,26 +1885,6 @@ web:
}
]
@data(
'2 ',
'3.',
'3.0.0',
'3.0.a',
'3.a',
'3a')
def test_invalid_version_formats(self, version):
content = {
'version': version,
'services': {
'web': {
'image': 'alpine',
}
}
}
with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details(content))
assert 'Version "{}" in "filename.yml" is invalid.'.format(version) in exc.exconly()
def test_group_add_option(self):
actual = config.load(build_config_details({
'version': '2',
@@ -4727,48 +4701,43 @@ class ExtendsTest(unittest.TestCase):
@mock.patch.dict(os.environ)
def test_extends_with_environment_and_env_files(self):
tmpdir = tempfile.mkdtemp('test_extends_with_environment')
self.addCleanup(shutil.rmtree, tmpdir)
commondir = os.path.join(tmpdir, 'common')
os.mkdir(commondir)
with open(os.path.join(commondir, 'base.yml'), mode="w") as base_fh:
base_fh.write("""
app:
image: 'example/app'
env_file:
- 'envs'
environment:
- SECRET
- TEST_ONE=common
- TEST_TWO=common
""")
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
ext:
extends:
file: common/base.yml
service: app
env_file:
- 'envs'
environment:
- THING
- TEST_ONE=top
""")
with open(os.path.join(commondir, 'envs'), mode="w") as envs_fh:
envs_fh.write("""
COMMON_ENV_FILE
TEST_ONE=common-env-file
TEST_TWO=common-env-file
TEST_THREE=common-env-file
TEST_FOUR=common-env-file
""")
with open(os.path.join(tmpdir, 'envs'), mode="w") as envs_fh:
envs_fh.write("""
TOP_ENV_FILE
TEST_ONE=top-env-file
TEST_TWO=top-env-file
TEST_THREE=top-env-file
""")
tmpdir = py.test.ensuretemp('test_extends_with_environment')
self.addCleanup(tmpdir.remove)
commondir = tmpdir.mkdir('common')
commondir.join('base.yml').write("""
app:
image: 'example/app'
env_file:
- 'envs'
environment:
- SECRET
- TEST_ONE=common
- TEST_TWO=common
""")
tmpdir.join('docker-compose.yml').write("""
ext:
extends:
file: common/base.yml
service: app
env_file:
- 'envs'
environment:
- THING
- TEST_ONE=top
""")
commondir.join('envs').write("""
COMMON_ENV_FILE
TEST_ONE=common-env-file
TEST_TWO=common-env-file
TEST_THREE=common-env-file
TEST_FOUR=common-env-file
""")
tmpdir.join('envs').write("""
TOP_ENV_FILE
TEST_ONE=top-env-file
TEST_TWO=top-env-file
TEST_THREE=top-env-file
""")
expected = [
{
@@ -4791,77 +4760,72 @@ class ExtendsTest(unittest.TestCase):
os.environ['THING'] = 'thing'
os.environ['COMMON_ENV_FILE'] = 'secret'
os.environ['TOP_ENV_FILE'] = 'secret'
config = load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
config = load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert config == expected
def test_extends_with_mixed_versions_is_error(self):
tmpdir = tempfile.mkdtemp('test_extends_with_mixed_version')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
version: "2"
services:
web:
extends:
file: base.yml
service: base
image: busybox
""")
with open(os.path.join(tmpdir, 'base.yml'), mode="w") as base_fh:
base_fh.write("""
base:
volumes: ['/foo']
ports: ['3000:3000']
""")
tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: "2"
services:
web:
extends:
file: base.yml
service: base
image: busybox
""")
tmpdir.join('base.yml').write("""
base:
volumes: ['/foo']
ports: ['3000:3000']
""")
with pytest.raises(ConfigurationError) as exc:
load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert 'Version mismatch' in exc.exconly()
def test_extends_with_defined_version_passes(self):
tmpdir = tempfile.mkdtemp('test_extends_with_defined_version')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
version: "2"
services:
web:
extends:
file: base.yml
service: base
image: busybox
""")
with open(os.path.join(tmpdir, 'base.yml'), mode="w") as base_fh:
base_fh.write("""
version: "2"
services:
base:
volumes: ['/foo']
ports: ['3000:3000']
command: top
""")
tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: "2"
services:
web:
extends:
file: base.yml
service: base
image: busybox
""")
tmpdir.join('base.yml').write("""
version: "2"
services:
base:
volumes: ['/foo']
ports: ['3000:3000']
command: top
""")
service = load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
service = load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert service[0]['command'] == "top"
def test_extends_with_depends_on(self):
tmpdir = tempfile.mkdtemp('test_extends_with_depends_on')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
version: "2"
services:
base:
image: example
web:
extends: base
image: busybox
depends_on: ['other']
other:
image: example
""")
services = load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
tmpdir = py.test.ensuretemp('test_extends_with_depends_on')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: "2"
services:
base:
image: example
web:
extends: base
image: busybox
depends_on: ['other']
other:
image: example
""")
services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert service_sort(services)[2]['depends_on'] == {
'other': {'condition': 'service_started'}
}
@@ -4880,47 +4844,45 @@ class ExtendsTest(unittest.TestCase):
}]
def test_extends_with_ports(self):
tmpdir = tempfile.mkdtemp('test_extends_with_ports')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
version: '2'
tmpdir = py.test.ensuretemp('test_extends_with_ports')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: '2'
services:
a:
image: nginx
ports:
- 80
services:
a:
image: nginx
ports:
- 80
b:
extends:
service: a
""")
services = load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
b:
extends:
service: a
""")
services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert len(services) == 2
for svc in services:
assert svc['ports'] == [types.ServicePort('80', None, None, None, None)]
def test_extends_with_security_opt(self):
tmpdir = tempfile.mkdtemp('test_extends_with_ports')
self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'docker-compose.yml'), mode="w") as docker_compose_fh:
docker_compose_fh.write("""
version: '2'
tmpdir = py.test.ensuretemp('test_extends_with_ports')
self.addCleanup(tmpdir.remove)
tmpdir.join('docker-compose.yml').write("""
version: '2'
services:
a:
image: nginx
security_opt:
- apparmor:unconfined
- seccomp:unconfined
services:
a:
image: nginx
security_opt:
- apparmor:unconfined
- seccomp:unconfined
b:
extends:
service: a
""")
services = load_from_filename(str(os.path.join(tmpdir, 'docker-compose.yml')))
b:
extends:
service: a
""")
services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
assert len(services) == 2
for svc in services:
assert types.SecurityOpt.parse('apparmor:unconfined') in svc['security_opt']
@@ -5075,7 +5037,7 @@ class HealthcheckTest(unittest.TestCase):
})
)
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['test']
assert serialized_service['healthcheck'] == {
@@ -5102,7 +5064,7 @@ class HealthcheckTest(unittest.TestCase):
})
)
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['test']
assert serialized_service['healthcheck'] == {
@@ -5309,7 +5271,7 @@ class SerializeTest(unittest.TestCase):
'secrets': secrets_dict
}))
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['web']
assert secret_sort(serialized_service['secrets']) == secret_sort(service_dict['secrets'])
assert 'secrets' in serialized_config
@@ -5324,7 +5286,7 @@ class SerializeTest(unittest.TestCase):
}
], volumes={}, networks={}, secrets={}, configs={})
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
assert '8080:80/tcp' in serialized_config['services']['web']['ports']
def test_serialize_ports_with_ext_ip(self):
@@ -5336,7 +5298,7 @@ class SerializeTest(unittest.TestCase):
}
], volumes={}, networks={}, secrets={}, configs={})
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
assert '127.0.0.1:8080:80/tcp' in serialized_config['services']['web']['ports']
def test_serialize_configs(self):
@@ -5364,7 +5326,7 @@ class SerializeTest(unittest.TestCase):
'configs': configs_dict
}))
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['web']
assert secret_sort(serialized_service['configs']) == secret_sort(service_dict['configs'])
assert 'configs' in serialized_config
@@ -5404,7 +5366,7 @@ class SerializeTest(unittest.TestCase):
}
config_dict = config.load(build_config_details(cfg))
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['web']
assert serialized_service['environment']['CURRENCY'] == '$$'
assert serialized_service['command'] == 'echo $$FOO'
@@ -5426,7 +5388,7 @@ class SerializeTest(unittest.TestCase):
}
config_dict = config.load(build_config_details(cfg), interpolate=False)
serialized_config = yaml.safe_load(serialize_config(config_dict, escape_dollar=False))
serialized_config = yaml.load(serialize_config(config_dict, escape_dollar=False))
serialized_service = serialized_config['services']['web']
assert serialized_service['environment']['CURRENCY'] == '$'
assert serialized_service['command'] == 'echo $FOO'
@@ -5445,7 +5407,7 @@ class SerializeTest(unittest.TestCase):
config_dict = config.load(build_config_details(cfg))
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_service = serialized_config['services']['web']
assert serialized_service['command'] == 'echo 十六夜 咲夜'
@@ -5461,6 +5423,6 @@ class SerializeTest(unittest.TestCase):
}
config_dict = config.load(build_config_details(cfg))
serialized_config = yaml.safe_load(serialize_config(config_dict))
serialized_config = yaml.load(serialize_config(config_dict))
serialized_volume = serialized_config['volumes']['test']
assert serialized_volume['external'] is False

View File

@@ -4,9 +4,6 @@ from __future__ import print_function
from __future__ import unicode_literals
import codecs
import os
import shutil
import tempfile
import pytest
@@ -49,19 +46,19 @@ class EnvironmentTest(unittest.TestCase):
assert env.get_boolean('UNDEFINED') is False
def test_env_vars_from_file_bom(self):
tmpdir = tempfile.mkdtemp('env_file')
self.addCleanup(shutil.rmtree, tmpdir)
tmpdir = pytest.ensuretemp('env_file')
self.addCleanup(tmpdir.remove)
with codecs.open('{}/bom.env'.format(str(tmpdir)), 'w', encoding='utf-8') as f:
f.write('\ufeffPARK_BOM=박봄\n')
assert env_vars_from_file(str(os.path.join(tmpdir, 'bom.env'))) == {
assert env_vars_from_file(str(tmpdir.join('bom.env'))) == {
'PARK_BOM': '박봄'
}
def test_env_vars_from_file_whitespace(self):
tmpdir = tempfile.mkdtemp('env_file')
self.addCleanup(shutil.rmtree, tmpdir)
tmpdir = pytest.ensuretemp('env_file')
self.addCleanup(tmpdir.remove)
with codecs.open('{}/whitespace.env'.format(str(tmpdir)), 'w', encoding='utf-8') as f:
f.write('WHITESPACE =yes\n')
with pytest.raises(ConfigurationError) as exc:
env_vars_from_file(str(os.path.join(tmpdir, 'whitespace.env')))
env_vars_from_file(str(tmpdir.join('whitespace.env')))
assert 'environment variable' in exc.exconly()

View File

@@ -521,37 +521,7 @@ class ServiceTest(unittest.TestCase):
assert 'was built because it did not already exist' in args[0]
assert self.mock_client.build.call_count == 1
assert self.mock_client.build.call_args[1]['tag'] == 'default_foo'
def test_create_container_binary_string_error(self):
service = Service('foo', client=self.mock_client, build={'context': '.'})
service.image = lambda: {'Id': 'abc123'}
self.mock_client.create_container.side_effect = APIError(None,
None,
b"Test binary string explanation")
with pytest.raises(OperationFailedError) as ex:
service.create_container()
assert ex.value.msg == "Cannot create container for service foo: Test binary string explanation"
def test_start_binary_string_error(self):
service = Service('foo', client=self.mock_client)
container = Container(self.mock_client, {'Id': 'abc123'})
self.mock_client.start.side_effect = APIError(None,
None,
b"Test binary string explanation with "
b"driver failed programming external "
b"connectivity")
with mock.patch('compose.service.log', autospec=True) as mock_log:
with pytest.raises(OperationFailedError) as ex:
service.start_container(container)
assert ex.value.msg == "Cannot start service foo: " \
"Test binary string explanation " \
"with driver failed programming external connectivity"
mock_log.warn.assert_called_once_with("Host is already in use by another container")
self.mock_client.build.call_args[1]['tag'] == 'default_foo'
def test_ensure_image_exists_no_build(self):
service = Service('foo', client=self.mock_client, build={'context': '.'})