mirror of
https://github.com/docker/compose.git
synced 2026-02-13 03:59:29 +08:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c64b044b7e | ||
|
|
1a4e6c2465 | ||
|
|
67b4669f9b | ||
|
|
61735c0012 | ||
|
|
c12a948f97 | ||
|
|
bf5785307b | ||
|
|
52eeda9aa7 | ||
|
|
1f0cf0723c | ||
|
|
315f16e5fb | ||
|
|
327a1bb27b | ||
|
|
16914e372e | ||
|
|
ec080f184a | ||
|
|
b5b8e7a116 | ||
|
|
9d73cc88cc | ||
|
|
9c68c76bea | ||
|
|
aeb7448449 | ||
|
|
42c3adb236 | ||
|
|
35f37cd1f7 | ||
|
|
c0465616bb | ||
|
|
32d44dfc25 | ||
|
|
5885a250bc | ||
|
|
ce1c788237 | ||
|
|
67f7b84829 | ||
|
|
fd676adc5d | ||
|
|
aa864fde20 | ||
|
|
f7a6c3bc54 | ||
|
|
8a9498c571 | ||
|
|
7e7262bc77 | ||
|
|
981aea674d | ||
|
|
64a9e4bf01 | ||
|
|
4be38f84df | ||
|
|
09e0fa94b8 | ||
|
|
416498441c | ||
|
|
cb45c6f2df | ||
|
|
90ca37344f | ||
|
|
fb3f9e270f | ||
|
|
02f78d2893 | ||
|
|
d47dcef1a6 | ||
|
|
fb9310caf2 | ||
|
|
ced9eba940 | ||
|
|
2eeed8481d | ||
|
|
10ca0314bc | ||
|
|
336b825fdd | ||
|
|
213d9166dc | ||
|
|
598b59f8bf | ||
|
|
65ed8cf4c2 | ||
|
|
a23cbb580e | ||
|
|
a89e194558 | ||
|
|
b31695a66e | ||
|
|
5262d3bbf5 | ||
|
|
a4836391a5 | ||
|
|
bfd7428619 | ||
|
|
feba34e406 | ||
|
|
37f763f009 | ||
|
|
99cd90a4b2 | ||
|
|
a279c3a934 | ||
|
|
ee586e7f1e | ||
|
|
5eb314a4ca | ||
|
|
c5cdce0b60 | ||
|
|
6dc6bedb60 | ||
|
|
9f06a02eb5 | ||
|
|
9d0421a929 | ||
|
|
340b5482b0 | ||
|
|
faaa93bf12 | ||
|
|
381df20010 | ||
|
|
a9e8164a8d | ||
|
|
1191023fb6 | ||
|
|
a108690ac2 | ||
|
|
0e81d1c88e | ||
|
|
22002531d8 | ||
|
|
b66ff0c3d8 | ||
|
|
7b6439997d | ||
|
|
d37b3fe413 | ||
|
|
ce7a2412b1 | ||
|
|
9e52e9fbe3 | ||
|
|
c5b7624d10 | ||
|
|
cf7319fc6e | ||
|
|
740276f550 | ||
|
|
40bca10250 | ||
|
|
8ae8d99528 | ||
|
|
f03b7085c3 | ||
|
|
36c2947e4d | ||
|
|
3bbcc3d4d0 | ||
|
|
b47d8ea868 | ||
|
|
a97a73600e | ||
|
|
6735220557 | ||
|
|
d7d29b25bc | ||
|
|
e2f33af831 | ||
|
|
19b9fdf536 | ||
|
|
a842522f49 | ||
|
|
bc1160de72 | ||
|
|
f791bc8a42 | ||
|
|
0d7567131a | ||
|
|
7b84f2c2a5 | ||
|
|
cf7b1441d9 | ||
|
|
32005b0bfe | ||
|
|
025a72a417 | ||
|
|
95c4502b81 | ||
|
|
2290ce2c24 | ||
|
|
bac732837e | ||
|
|
b725c56c42 | ||
|
|
cfcc9533b3 | ||
|
|
cffdb69c5e | ||
|
|
709190312c | ||
|
|
e1a38f984b | ||
|
|
4dafeb57a5 | ||
|
|
45956c36fb | ||
|
|
9eb69465b7 | ||
|
|
5f392258cb | ||
|
|
382c1cd68e | ||
|
|
5994050f51 | ||
|
|
28a00571ef | ||
|
|
d00eacbba0 | ||
|
|
a6c76a9c0f | ||
|
|
5754d6084c | ||
|
|
10cd7e130f | ||
|
|
8f9dc2e7f8 | ||
|
|
dfa93d834f | ||
|
|
f69a613e69 | ||
|
|
a8fbbd9e5c | ||
|
|
ba724576a1 | ||
|
|
3261b60fd1 | ||
|
|
cb425a23c0 | ||
|
|
29179840c3 | ||
|
|
7205d918ad | ||
|
|
413f46ade0 | ||
|
|
8a9c4b52b4 | ||
|
|
865b82da6a | ||
|
|
e44222664a | ||
|
|
d4e3f191ac | ||
|
|
577bee955b | ||
|
|
ed2395819d | ||
|
|
e6599c7213 | ||
|
|
9c01e41adf | ||
|
|
6df30f39f2 | ||
|
|
a79346b978 | ||
|
|
125752c127 | ||
|
|
95f0431127 | ||
|
|
2bee75c3c4 | ||
|
|
a1f7be7b5c | ||
|
|
72e4519cbf | ||
|
|
9e6f51d262 | ||
|
|
98b3353cbc | ||
|
|
c756ff3d3e | ||
|
|
fc827f295b | ||
|
|
f10c96a54a | ||
|
|
06c5d8a902 | ||
|
|
0f3c214b48 | ||
|
|
058c779378 | ||
|
|
07a562aa2d | ||
|
|
76fe903deb | ||
|
|
284bad4411 | ||
|
|
04d8212a88 | ||
|
|
d38f278f68 | ||
|
|
4cb6ace1ce | ||
|
|
9d04f3ff73 | ||
|
|
cc5b72b11d | ||
|
|
ba08d39187 | ||
|
|
c4cfaeb12a | ||
|
|
b2d2c67032 | ||
|
|
9bc1dfe036 | ||
|
|
6476e10b93 | ||
|
|
2530bd981a | ||
|
|
d38a315798 | ||
|
|
e3204e7c4e | ||
|
|
27f5b8536b | ||
|
|
85ef72585d | ||
|
|
c3a5eb2269 | ||
|
|
94379769e3 | ||
|
|
7d768e7c1d | ||
|
|
ea5b094a93 | ||
|
|
555b0ab0da | ||
|
|
c2dd40c161 | ||
|
|
3260dcb121 | ||
|
|
55a214a45e | ||
|
|
35b4d1de28 | ||
|
|
d48068d6e1 | ||
|
|
ef786f9245 | ||
|
|
0062703bea | ||
|
|
c55fde7080 | ||
|
|
9ee66b8918 | ||
|
|
fc8a433cee | ||
|
|
98fe57baac | ||
|
|
fdb90ca179 | ||
|
|
1b106f9133 | ||
|
|
4af04b23ec | ||
|
|
0a81a98b7d | ||
|
|
8bba863935 | ||
|
|
7e6af46af0 | ||
|
|
693732c4a7 | ||
|
|
435387c72c | ||
|
|
ecee21b5e5 | ||
|
|
7c0e865960 | ||
|
|
c65aed3f9d | ||
|
|
1faa76ee4e | ||
|
|
7365917244 | ||
|
|
d2af460d81 | ||
|
|
14e42df8f2 | ||
|
|
17354fcc99 | ||
|
|
769ff110f3 | ||
|
|
3dcbc13742 | ||
|
|
7f31fe678f | ||
|
|
6e9d9bf86d | ||
|
|
9e749fa03d | ||
|
|
436c588793 | ||
|
|
fdf89e0e16 | ||
|
|
2b8fd90021 | ||
|
|
17d845b3d2 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -20,7 +20,7 @@ The GitHub issue tracker is for bug reports and feature requests.
|
||||
General support can be found at the following locations:
|
||||
|
||||
- Docker Support Forums - https://forums.docker.com
|
||||
- Docker Community Slack - https://dockr.ly/community
|
||||
- Docker Community Slack - https://dockr.ly/slack
|
||||
- Post a question on StackOverflow, using the Docker tag
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
5
.github/workflows/artifacts.yml
vendored
5
.github/workflows/artifacts.yml
vendored
@@ -7,10 +7,10 @@ jobs:
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/generate-artifacts')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout code into the Go module directory
|
||||
@@ -55,4 +55,3 @@ jobs:
|
||||
body: |
|
||||
This PR can be tested using [binaries](https://github.com/docker/compose-cli/actions/runs/${{ github.run_id }}).
|
||||
reactions: eyes
|
||||
|
||||
|
||||
63
.github/workflows/ci.yml
vendored
63
.github/workflows/ci.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout code into the Go module directory
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
- name: Run golangci-lint
|
||||
env:
|
||||
BUILD_TAGS: e2e
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin/ v1.39.0
|
||||
make -f builder.Makefile lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
args: --timeout=180s
|
||||
|
||||
# only on main branch, costs too much for the gain on every PR
|
||||
validate-cross-build:
|
||||
@@ -40,10 +40,10 @@ jobs:
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout code into the Go module directory
|
||||
@@ -59,22 +59,18 @@ jobs:
|
||||
- name: Build packages
|
||||
run: make -f builder.Makefile cross
|
||||
|
||||
build:
|
||||
name: Build
|
||||
build-plugin:
|
||||
name: Build and tests in plugin mode
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Set up gosum
|
||||
run: |
|
||||
go get -u gotest.tools/gotestsum
|
||||
|
||||
- name: Setup docker CLI
|
||||
run: |
|
||||
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
|
||||
@@ -89,8 +85,6 @@ jobs:
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
BUILD_TAGS: kube
|
||||
run: make -f builder.Makefile test
|
||||
|
||||
- name: Build for local E2E
|
||||
@@ -98,5 +92,38 @@ jobs:
|
||||
BUILD_TAGS: e2e
|
||||
run: make -f builder.Makefile compose-plugin
|
||||
|
||||
- name: E2E Test
|
||||
- name: E2E Test in plugin mode
|
||||
run: make e2e-compose
|
||||
|
||||
build-standalone:
|
||||
name: Build and tests in standalone mode
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Setup docker CLI
|
||||
run: |
|
||||
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
|
||||
sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
|
||||
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Build for local E2E
|
||||
env:
|
||||
BUILD_TAGS: e2e
|
||||
run: make -f builder.Makefile compose-plugin
|
||||
|
||||
- name: E2E Test in standalone mode
|
||||
run: make e2e-compose-standalone
|
||||
|
||||
9
.github/workflows/release.yaml
vendored
9
.github/workflows/release.yaml
vendored
@@ -4,17 +4,17 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release Tag'
|
||||
description: "Release Tag"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
upload-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.16
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Setup docker CLI
|
||||
@@ -35,6 +35,9 @@ jobs:
|
||||
- name: Build
|
||||
run: make GIT_TAG=${{ github.event.inputs.tag }} -f builder.Makefile cross
|
||||
|
||||
- name: Compute checksums
|
||||
run: cd bin; for f in *; do shasum --algorithm 256 $f > $f.sha256; done
|
||||
|
||||
- name: License
|
||||
run: cp packaging/* bin/
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ don't get discouraged! Our contributor's guide explains
|
||||
<tr>
|
||||
<td>Community Slack</td>
|
||||
<td>
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockercommunity.slack.com/ssb/redirect" target="_blank">with this link</a>.
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/docker-community" target="_blank">with this link</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.16-alpine
|
||||
ARG GO_VERSION=1.17-alpine
|
||||
ARG GOLANGCI_LINT_VERSION=v1.40.1-alpine
|
||||
ARG PROTOC_GEN_GO_VERSION=v1.4.3
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
people = [
|
||||
"rumpl",
|
||||
"gtardif",
|
||||
"ndeloof"
|
||||
"chris-crone"
|
||||
"ndeloof",
|
||||
"chris-crone",
|
||||
"ulyssessouza"
|
||||
]
|
||||
|
||||
|
||||
12
Makefile
12
Makefile
@@ -42,8 +42,16 @@ compose-plugin: ## Compile the compose cli-plugin
|
||||
--output ./bin
|
||||
|
||||
.PHONY: e2e-compose
|
||||
e2e-compose: ## Run End to end local tests. Set E2E_TEST=TestName to run a single test
|
||||
gotestsum $(TEST_FLAGS) ./pkg/e2e -- -count=1
|
||||
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
go test $(TEST_FLAGS) -count=1 ./pkg/e2e
|
||||
|
||||
.PHONY: e2e-compose-standalone
|
||||
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
|
||||
go test $(TEST_FLAGS) -count=1 --tags=standalone ./pkg/e2e
|
||||
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: e2e-compose e2e-compose-standalone ## Run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: cross
|
||||
cross: ## Compile the CLI for linux, darwin and windows
|
||||
|
||||
19
README.md
19
README.md
@@ -9,7 +9,13 @@ defined using the [Compose file format](https://compose-spec.io).
|
||||
A Compose file is used to define how the one or more containers that make up
|
||||
your application are configured.
|
||||
Once you have a Compose file, you can create and start your application with a
|
||||
single command: `docker-compose up`.
|
||||
single command: `docker compose up`.
|
||||
|
||||
# About update and backward compatibility
|
||||
|
||||
Docker Compose V2 is a major version bump release of Docker Compose. It has been completely rewritten from scratch in Golang (V1 was in Python). The installation instructions for Compose V2 differ from V1. V2 is not a standalone binary anymore, and installation scripts will have to be adjusted. Some commands are different.
|
||||
|
||||
For a smooth transition from legacy docker-compose 1.xx, please consider installing [compose-switch](https://github.com/docker/compose-switch) to translate `docker-compose ...` commands into Compose V2's `docker compose .... `. Also check V2's `--compatibility` flag.
|
||||
|
||||
# Where to get Docker Compose
|
||||
|
||||
@@ -24,9 +30,16 @@ for Windows and macOS.
|
||||
You can download Docker Compose binaries from the
|
||||
[release page](https://github.com/docker/compose/releases) on this repository.
|
||||
|
||||
Copy the relevant binary for your OS under `$HOME/.docker/cli-plugins/docker-compose`
|
||||
Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins`
|
||||
|
||||
Or copy it into one of these folders for installing it system-wide:
|
||||
|
||||
* `/usr/local/lib/docker/cli-plugins` OR `/usr/local/libexec/docker/cli-plugins`
|
||||
* `/usr/lib/docker/cli-plugins` OR `/usr/libexec/docker/cli-plugins`
|
||||
|
||||
(might require to make the downloaded file executable with `chmod +x`)
|
||||
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
@@ -35,7 +48,7 @@ Using Docker Compose is basically a three-step process:
|
||||
reproduced anywhere.
|
||||
2. Define the services that make up your app in `docker-compose.yml` so
|
||||
they can be run together in an isolated environment.
|
||||
3. Lastly, run `docker-compose up` and Compose will start and run your entire
|
||||
3. Lastly, run `docker compose up` and Compose will start and run your entire
|
||||
app.
|
||||
|
||||
A Compose file looks like this:
|
||||
|
||||
@@ -46,14 +46,14 @@ compose-plugin:
|
||||
|
||||
.PHONY: cross
|
||||
cross:
|
||||
GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-amd64 ./cmd
|
||||
GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-arm64 ./cmd
|
||||
GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-x86_64 ./cmd
|
||||
GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-aarch64 ./cmd
|
||||
GOOS=linux GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv6 ./cmd
|
||||
GOOS=linux GOARM=7 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv7 ./cmd
|
||||
GOOS=linux GOARCH=s390x $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-s390x ./cmd
|
||||
GOOS=darwin GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-amd64 ./cmd
|
||||
GOOS=darwin GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-arm64 ./cmd
|
||||
GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-windows-amd64.exe ./cmd
|
||||
GOOS=darwin GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-x86_64 ./cmd
|
||||
GOOS=darwin GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-aarch64 ./cmd
|
||||
GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-windows-x86_64.exe ./cmd
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
|
||||
97
cmd/compatibility/convert.go
Normal file
97
cmd/compatibility/convert.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compatibility
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compose"
|
||||
)
|
||||
|
||||
func getBoolFlags() []string {
|
||||
return []string{
|
||||
"--debug", "-D",
|
||||
"--verbose",
|
||||
"--tls",
|
||||
"--tlsverify",
|
||||
}
|
||||
}
|
||||
|
||||
func getStringFlags() []string {
|
||||
return []string{
|
||||
"--tlscacert",
|
||||
"--tlscert",
|
||||
"--tlskey",
|
||||
"--host", "-H",
|
||||
"--context",
|
||||
"--log-level",
|
||||
}
|
||||
}
|
||||
|
||||
// Convert transforms standalone docker-compose args into CLI plugin compliant ones
|
||||
func Convert(args []string) []string {
|
||||
var rootFlags []string
|
||||
command := []string{compose.PluginName}
|
||||
l := len(args)
|
||||
for i := 0; i < l; i++ {
|
||||
arg := args[i]
|
||||
if arg[0] != '-' {
|
||||
// not a top-level flag anymore, keep the rest of the command unmodified
|
||||
if arg == compose.PluginName {
|
||||
i++
|
||||
}
|
||||
command = append(command, args[i:]...)
|
||||
break
|
||||
}
|
||||
if arg == "--verbose" {
|
||||
arg = "--debug"
|
||||
}
|
||||
if arg == "-h" {
|
||||
// docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it
|
||||
arg = "--help"
|
||||
}
|
||||
if arg == "--version" || arg == "-v" {
|
||||
// redirect --version pseudo-command to actual command
|
||||
arg = "version"
|
||||
}
|
||||
if contains(getBoolFlags(), arg) {
|
||||
rootFlags = append(rootFlags, arg)
|
||||
continue
|
||||
}
|
||||
if contains(getStringFlags(), arg) {
|
||||
i++
|
||||
if i >= l {
|
||||
fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
rootFlags = append(rootFlags, arg, args[i])
|
||||
continue
|
||||
}
|
||||
command = append(command, arg)
|
||||
}
|
||||
return append(rootFlags, command...)
|
||||
}
|
||||
|
||||
func contains(array []string, needle string) bool {
|
||||
for _, val := range array {
|
||||
if val == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
78
cmd/compatibility/convert_test.go
Normal file
78
cmd/compatibility/convert_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compatibility
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func Test_convert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "compose only",
|
||||
args: []string{"up"},
|
||||
want: []string{"compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "with context",
|
||||
args: []string{"--context", "foo", "-f", "compose.yaml", "up"},
|
||||
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
|
||||
},
|
||||
{
|
||||
name: "with host",
|
||||
args: []string{"--host", "tcp://1.2.3.4", "up"},
|
||||
want: []string{"--host", "tcp://1.2.3.4", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "compose --version",
|
||||
args: []string{"--version"},
|
||||
want: []string{"compose", "version"},
|
||||
},
|
||||
{
|
||||
name: "help",
|
||||
args: []string{"-h"},
|
||||
want: []string{"compose", "--help"},
|
||||
},
|
||||
{
|
||||
name: "issues/1962",
|
||||
args: []string{"psql", "-h", "postgres"},
|
||||
want: []string{"compose", "psql", "-h", "postgres"}, // -h should not be converted to --help
|
||||
},
|
||||
{
|
||||
name: "issues/8648",
|
||||
args: []string{"exec", "mongo", "mongo", "--host", "mongo"},
|
||||
want: []string{"compose", "exec", "mongo", "mongo", "--host", "mongo"}, // --host is passed to exec
|
||||
},
|
||||
{
|
||||
name: "issues/12",
|
||||
args: []string{"--log-level", "INFO", "up"},
|
||||
want: []string{"--log-level", "INFO", "compose", "up"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Convert(tt.args)
|
||||
assert.DeepEqual(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -39,6 +42,13 @@ type buildOptions struct {
|
||||
memory string
|
||||
}
|
||||
|
||||
var printerModes = []string{
|
||||
buildx.PrinterModeAuto,
|
||||
buildx.PrinterModeTty,
|
||||
buildx.PrinterModePlain,
|
||||
buildx.PrinterModeQuiet,
|
||||
}
|
||||
|
||||
func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
opts := buildOptions{
|
||||
projectOptions: p,
|
||||
@@ -51,12 +61,16 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
fmt.Println("WARNING --memory is ignored as not supported in buildkit.")
|
||||
}
|
||||
if opts.quiet {
|
||||
opts.progress = buildx.PrinterModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
if !utils.StringContains(printerModes, opts.progress) {
|
||||
return fmt.Errorf("unsupported --progress value %q", opts.progress)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
@@ -66,7 +80,7 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
|
||||
cmd.Flags().StringVar(&opts.progress, "progress", "auto", `Set type of progress output ("auto", "plain", "noTty")`)
|
||||
cmd.Flags().StringVar(&opts.progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.")
|
||||
cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("parallel") //nolint:errcheck
|
||||
|
||||
@@ -25,15 +25,15 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
@@ -106,7 +106,7 @@ type ProjectFunc func(ctx context.Context, project *types.Project) error
|
||||
// ProjectServicesFunc does stuff within a types.Project and a selection of services
|
||||
type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
|
||||
|
||||
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
|
||||
// WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
|
||||
func (o *projectOptions) WithProject(fn ProjectFunc) func(cmd *cobra.Command, args []string) error {
|
||||
return o.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
|
||||
return fn(ctx, project)
|
||||
@@ -121,24 +121,6 @@ func (o *projectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Co
|
||||
return err
|
||||
}
|
||||
|
||||
if o.EnvFile != "" {
|
||||
var services types.Services
|
||||
for _, s := range project.Services {
|
||||
ef := o.EnvFile
|
||||
if ef != "" {
|
||||
if !filepath.IsAbs(ef) {
|
||||
ef = filepath.Join(project.WorkingDir, o.EnvFile)
|
||||
}
|
||||
if s.Labels == nil {
|
||||
s.Labels = make(map[string]string)
|
||||
}
|
||||
s.Labels[api.EnvironmentFileLabel] = ef
|
||||
services = append(services, s)
|
||||
}
|
||||
}
|
||||
project.Services = services
|
||||
}
|
||||
|
||||
return fn(ctx, project, args)
|
||||
})
|
||||
}
|
||||
@@ -177,6 +159,29 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
|
||||
return nil, compose.WrapComposeError(err)
|
||||
}
|
||||
|
||||
if o.Compatibility || utils.StringToBool(project.Environment["COMPOSE_COMPATIBILITY"]) {
|
||||
compose.Separator = "_"
|
||||
}
|
||||
|
||||
ef := o.EnvFile
|
||||
if ef != "" && !filepath.IsAbs(ef) {
|
||||
ef = filepath.Join(project.WorkingDir, o.EnvFile)
|
||||
}
|
||||
for i, s := range project.Services {
|
||||
s.CustomLabels = map[string]string{
|
||||
api.ProjectLabel: project.Name,
|
||||
api.ServiceLabel: s.Name,
|
||||
api.VersionLabel: api.ComposeVersion,
|
||||
api.WorkingDirLabel: project.WorkingDir,
|
||||
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
|
||||
api.OneoffLabel: "False", // default, will be overridden by `run` command
|
||||
}
|
||||
if ef != "" {
|
||||
s.CustomLabels[api.EnvironmentFileLabel] = ef
|
||||
}
|
||||
project.Services[i] = s
|
||||
}
|
||||
|
||||
if len(services) > 0 {
|
||||
s, err := project.GetServices(services...)
|
||||
if err != nil {
|
||||
@@ -200,15 +205,23 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
|
||||
func (o *projectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
|
||||
return cli.NewProjectOptions(o.ConfigPaths,
|
||||
append(po,
|
||||
cli.WithWorkingDirectory(o.ProjectDir),
|
||||
cli.WithEnvFile(o.EnvFile),
|
||||
cli.WithDotEnv,
|
||||
cli.WithOsEnv,
|
||||
cli.WithWorkingDirectory(o.ProjectDir),
|
||||
cli.WithConfigFileEnv,
|
||||
cli.WithDefaultConfigPath,
|
||||
cli.WithName(o.ProjectName))...)
|
||||
}
|
||||
|
||||
// PluginName is the name of the plugin
|
||||
const PluginName = "compose"
|
||||
|
||||
// RunningAsStandalone detects when running as a standalone program
|
||||
func RunningAsStandalone() bool {
|
||||
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
|
||||
}
|
||||
|
||||
// RootCommand returns the compose command with its child commands
|
||||
func RootCommand(backend api.Service) *cobra.Command {
|
||||
opts := projectOptions{}
|
||||
@@ -216,16 +229,20 @@ func RootCommand(backend api.Service) *cobra.Command {
|
||||
ansi string
|
||||
noAnsi bool
|
||||
verbose bool
|
||||
version bool
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Short: "Docker Compose",
|
||||
Use: "compose",
|
||||
Use: PluginName,
|
||||
TraverseChildren: true,
|
||||
// By default (no Run/RunE in parent command) for typos in subcommands, cobra displays the help of parent command but exit(0) !
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
if version {
|
||||
return versionCommand().Execute()
|
||||
}
|
||||
_ = cmd.Help()
|
||||
return dockercli.StatusError{
|
||||
StatusCode: compose.CommandSyntaxFailure.ExitCode,
|
||||
@@ -234,11 +251,13 @@ func RootCommand(backend api.Service) *cobra.Command {
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
parent := cmd.Root()
|
||||
parentPrerun := parent.PersistentPreRunE
|
||||
if parentPrerun != nil {
|
||||
err := parentPrerun(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
if parent != nil {
|
||||
parentPrerun := parent.PersistentPreRunE
|
||||
if parentPrerun != nil {
|
||||
err := parentPrerun(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if noAnsi {
|
||||
@@ -259,9 +278,6 @@ func RootCommand(backend api.Service) *cobra.Command {
|
||||
opts.ProjectDir = opts.WorkDir
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
|
||||
}
|
||||
if opts.Compatibility || os.Getenv("COMPOSE_COMPATIBILITY") == "true" {
|
||||
compose.Separator = "_"
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -296,6 +312,8 @@ func RootCommand(backend api.Service) *cobra.Command {
|
||||
command.Flags().SetInterspersed(false)
|
||||
opts.addProjectFlags(command.Flags())
|
||||
command.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
command.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
||||
command.Flags().MarkHidden("version") //nolint:errcheck
|
||||
command.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
|
||||
command.Flags().MarkHidden("no-ansi") //nolint:errcheck
|
||||
command.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
|
||||
|
||||
@@ -44,9 +44,11 @@ type convertOptions struct {
|
||||
quiet bool
|
||||
resolveImageDigests bool
|
||||
noInterpolate bool
|
||||
noNormalize bool
|
||||
services bool
|
||||
volumes bool
|
||||
profiles bool
|
||||
images bool
|
||||
hash string
|
||||
}
|
||||
|
||||
@@ -66,6 +68,9 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
if p.Compatibility {
|
||||
opts.noNormalize = true
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
@@ -81,6 +86,9 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
if opts.profiles {
|
||||
return runProfiles(opts, args)
|
||||
}
|
||||
if opts.images {
|
||||
return runConfigImages(opts, args)
|
||||
}
|
||||
|
||||
return runConvert(ctx, backend, opts, args)
|
||||
}),
|
||||
@@ -91,10 +99,12 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything.")
|
||||
flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables.")
|
||||
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model.")
|
||||
|
||||
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
|
||||
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
|
||||
flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
|
||||
flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
|
||||
flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
|
||||
flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
|
||||
|
||||
@@ -103,7 +113,12 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
|
||||
func runConvert(ctx context.Context, backend api.Service, opts convertOptions, services []string) error {
|
||||
var json []byte
|
||||
project, err := opts.toProject(services, cli.WithInterpolation(!opts.noInterpolate), cli.WithResolvedPaths(false))
|
||||
project, err := opts.toProject(services,
|
||||
cli.WithInterpolation(!opts.noInterpolate),
|
||||
cli.WithResolvedPaths(true),
|
||||
cli.WithNormalization(!opts.noNormalize),
|
||||
cli.WithDiscardEnvFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,3 +222,18 @@ func runProfiles(opts convertOptions, services []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfigImages(opts convertOptions, services []string) error {
|
||||
project, err := opts.toProject(services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range project.Services {
|
||||
if s.Image != "" {
|
||||
fmt.Println(s.Image)
|
||||
} else {
|
||||
fmt.Printf("%s_%s\n", project.Name, s.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ func copyCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, backend api.Service, opts copyOptions) error {
|
||||
projects, err := opts.toProject(nil)
|
||||
name, err := opts.toProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Copy(ctx, projects, api.CopyOptions{
|
||||
return backend.Copy(ctx, name, api.CopyOptions{
|
||||
Source: opts.source,
|
||||
Destination: opts.destination,
|
||||
All: opts.all,
|
||||
|
||||
@@ -19,10 +19,14 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
@@ -43,10 +47,8 @@ func downCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
downCmd := &cobra.Command{
|
||||
Use: "down",
|
||||
Short: "Stop and remove containers, networks",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.images != "" {
|
||||
if opts.images != "all" && opts.images != "local" {
|
||||
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
|
||||
@@ -60,10 +62,19 @@ func downCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
ValidArgsFunction: noCompletion(),
|
||||
}
|
||||
flags := downCmd.Flags()
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
|
||||
removeOrphans := utils.StringToBool(os.Getenv("COMPOSE_REMOVE_ORPHANS "))
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
|
||||
flags.BoolVarP(&opts.volumes, "volumes", "v", false, " Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.")
|
||||
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
switch name {
|
||||
case "volume":
|
||||
name = "volumes"
|
||||
logrus.Warn("--volume is deprecated, please use --volumes")
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
return downCmd
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type execOpts struct {
|
||||
@@ -69,23 +70,35 @@ func execCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].")
|
||||
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.")
|
||||
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.")
|
||||
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", notAtTTY(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
|
||||
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", false, "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
|
||||
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.")
|
||||
|
||||
runCmd.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached. DEPRECATED")
|
||||
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
|
||||
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY. DEPRECATED")
|
||||
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
runCmd.Flags().SetInterspersed(false)
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, backend api.Service, opts execOpts) error {
|
||||
project, err := opts.toProjectName()
|
||||
projectName, err := opts.toProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectOptions, err := opts.composeOptions.toProjectOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lookupFn := func(k string) (string, bool) {
|
||||
v, ok := projectOptions.Environment[k]
|
||||
return v, ok
|
||||
}
|
||||
execOpts := api.RunOptions{
|
||||
Service: opts.service,
|
||||
Command: opts.command,
|
||||
Environment: opts.environment,
|
||||
Environment: compose.ToMobyEnv(types.NewMappingWithEquals(opts.environment).Resolve(lookupFn)),
|
||||
Tty: !opts.noTty,
|
||||
User: opts.user,
|
||||
Privileged: opts.privileged,
|
||||
@@ -113,7 +126,7 @@ func runExec(ctx context.Context, backend api.Service, opts execOpts) error {
|
||||
execOpts.Stdout = con
|
||||
execOpts.Stderr = con
|
||||
}
|
||||
exitCode, err := backend.Exec(ctx, project, execOpts)
|
||||
exitCode, err := backend.Exec(ctx, projectName, execOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
|
||||
@@ -92,22 +92,24 @@ func runList(ctx context.Context, backend api.Service, opts lsOptions) error {
|
||||
view := viewFromStackList(stackList)
|
||||
return formatter.Print(view, opts.Format, os.Stdout, func(w io.Writer) {
|
||||
for _, stack := range view {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles)
|
||||
}
|
||||
}, "NAME", "STATUS")
|
||||
}, "NAME", "STATUS", "CONFIG FILES")
|
||||
}
|
||||
|
||||
type stackView struct {
|
||||
Name string
|
||||
Status string
|
||||
Name string
|
||||
Status string
|
||||
ConfigFiles string
|
||||
}
|
||||
|
||||
func viewFromStackList(stackList []api.Stack) []stackView {
|
||||
retList := make([]stackView, len(stackList))
|
||||
for i, s := range stackList {
|
||||
retList[i] = stackView{
|
||||
Name: s.Name,
|
||||
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
|
||||
Name: s.Name,
|
||||
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
|
||||
ConfigFiles: s.ConfigFiles,
|
||||
}
|
||||
}
|
||||
return retList
|
||||
|
||||
@@ -34,7 +34,7 @@ func pauseCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "pause [SERVICE...]",
|
||||
Short: "pause services",
|
||||
Short: "Pause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPause(ctx, backend, opts, args)
|
||||
}),
|
||||
@@ -64,7 +64,7 @@ func unpauseCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "unpause [SERVICE...]",
|
||||
Short: "unpause services",
|
||||
Short: "Unpause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runUnPause(ctx, backend, opts, args)
|
||||
}),
|
||||
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
|
||||
formatter2 "github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
@@ -42,7 +42,7 @@ type psOptions struct {
|
||||
Quiet bool
|
||||
Services bool
|
||||
Filter string
|
||||
Status string
|
||||
Status []string
|
||||
}
|
||||
|
||||
func (p *psOptions) parseFilter() error {
|
||||
@@ -55,11 +55,11 @@ func (p *psOptions) parseFilter() error {
|
||||
}
|
||||
switch parts[0] {
|
||||
case "status":
|
||||
p.Status = parts[1]
|
||||
p.Status = append(p.Status, parts[1])
|
||||
case "source":
|
||||
return api.ErrNotImplemented
|
||||
default:
|
||||
return fmt.Errorf("unknow filter %s", parts[0])
|
||||
return fmt.Errorf("unknown filter %s", parts[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func psCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags := psCmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json]")
|
||||
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property. Deprecated, use --status instead")
|
||||
flags.StringVar(&opts.Status, "status", "", "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
|
||||
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.BoolVar(&opts.Services, "services", false, "Display services")
|
||||
flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)")
|
||||
@@ -103,17 +103,6 @@ func runPs(ctx context.Context, backend api.Service, services []string, opts psO
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Services {
|
||||
services := []string{}
|
||||
for _, s := range containers {
|
||||
if !utils.StringContains(services, s.Service) {
|
||||
services = append(services, s.Service)
|
||||
}
|
||||
}
|
||||
fmt.Println(strings.Join(services, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
SERVICES:
|
||||
for _, s := range services {
|
||||
for _, c := range containers {
|
||||
@@ -124,7 +113,7 @@ SERVICES:
|
||||
return fmt.Errorf("no such service: %s", s)
|
||||
}
|
||||
|
||||
if opts.Status != "" {
|
||||
if len(opts.Status) != 0 {
|
||||
containers = filterByStatus(containers, opts.Status)
|
||||
}
|
||||
|
||||
@@ -139,6 +128,17 @@ SERVICES:
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Services {
|
||||
services := []string{}
|
||||
for _, s := range containers {
|
||||
if !utils.StringContains(services, s.Service) {
|
||||
services = append(services, s.Service)
|
||||
}
|
||||
}
|
||||
fmt.Println(strings.Join(services, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
return formatter.Print(containers, opts.Format, os.Stdout,
|
||||
writter(containers),
|
||||
"NAME", "COMMAND", "SERVICE", "STATUS", "PORTS")
|
||||
@@ -160,22 +160,25 @@ func writter(containers []api.ContainerSummary) func(w io.Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
func filterByStatus(containers []api.ContainerSummary, status string) []api.ContainerSummary {
|
||||
hasContainerWithState := map[string]struct{}{}
|
||||
for _, c := range containers {
|
||||
if c.State == status {
|
||||
hasContainerWithState[c.Service] = struct{}{}
|
||||
}
|
||||
}
|
||||
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
|
||||
var filtered []api.ContainerSummary
|
||||
for _, c := range containers {
|
||||
if _, ok := hasContainerWithState[c.Service]; ok {
|
||||
if hasStatus(c, statuses) {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func hasStatus(c api.ContainerSummary, statuses []string) bool {
|
||||
for _, status := range statuses {
|
||||
if c.State == status {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type portRange struct {
|
||||
pStart int
|
||||
pEnd int
|
||||
|
||||
@@ -65,7 +65,7 @@ func runRemove(ctx context.Context, backend api.Service, opts removeOptions, ser
|
||||
}
|
||||
|
||||
if opts.stop {
|
||||
err := backend.Stop(ctx, project, api.StopOptions{
|
||||
err := backend.Stop(ctx, project.Name, api.StopOptions{
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -49,13 +49,13 @@ func restartCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runRestart(ctx context.Context, backend api.Service, opts restartOptions, services []string) error {
|
||||
project, err := opts.toProject(services)
|
||||
projectName, err := opts.toProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := time.Duration(opts.timeout) * time.Second
|
||||
return backend.Restart(ctx, project, api.RestartOptions{
|
||||
return backend.Restart(ctx, projectName, api.RestartOptions{
|
||||
Timeout: &timeout,
|
||||
Services: services,
|
||||
})
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
cgo "github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/loader"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -54,6 +53,7 @@ type runOptions struct {
|
||||
servicePorts bool
|
||||
name string
|
||||
noDeps bool
|
||||
quietPull bool
|
||||
}
|
||||
|
||||
func (opts runOptions) apply(project *types.Project) error {
|
||||
@@ -143,7 +143,7 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
|
||||
flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label")
|
||||
flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
|
||||
flags.BoolVarP(&opts.noTty, "no-TTY", "T", notAtTTY(), "Disable pseudo-noTty allocation. By default docker compose run allocates a TTY")
|
||||
flags.BoolVarP(&opts.noTty, "no-TTY", "T", false, "Disable pseudo-noTty allocation. By default docker compose run allocates a TTY")
|
||||
flags.StringVar(&opts.name, "name", "", " Assign a name to the container")
|
||||
flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid")
|
||||
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
@@ -153,6 +153,12 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags.StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
|
||||
flags.BoolVar(&opts.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
|
||||
flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
|
||||
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.")
|
||||
|
||||
cmd.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("interactive") //nolint:errcheck
|
||||
cmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
flags.SetNormalizeFunc(normalizeRunFlags)
|
||||
flags.SetInterspersed(false)
|
||||
@@ -169,11 +175,6 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
func notAtTTY() bool {
|
||||
b := isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd())
|
||||
return !b
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions) error {
|
||||
err := opts.apply(project)
|
||||
if err != nil {
|
||||
@@ -215,6 +216,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
UseNetworkAliases: opts.useAliases,
|
||||
NoDeps: opts.noDeps,
|
||||
Index: 0,
|
||||
QuietPull: opts.quietPull,
|
||||
}
|
||||
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
|
||||
if exitCode != 0 {
|
||||
@@ -243,5 +245,5 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
|
||||
if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Start(ctx, &project, api.StartOptions{})
|
||||
return backend.Start(ctx, project.Name, api.StartOptions{})
|
||||
}
|
||||
|
||||
@@ -43,10 +43,12 @@ func startCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runStart(ctx context.Context, backend api.Service, opts startOptions, services []string) error {
|
||||
project, err := opts.toProject(services)
|
||||
projectName, err := opts.toProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Start(ctx, project, api.StartOptions{})
|
||||
return backend.Start(ctx, projectName, api.StartOptions{
|
||||
AttachTo: services,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func stopCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runStop(ctx context.Context, backend api.Service, opts stopOptions, services []string) error {
|
||||
project, err := opts.toProject(services)
|
||||
projectName, err := opts.toProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func runStop(ctx context.Context, backend api.Service, opts stopOptions, service
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
return backend.Stop(ctx, project, api.StopOptions{
|
||||
return backend.Stop(ctx, projectName, api.StopOptions{
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
})
|
||||
|
||||
@@ -40,7 +40,6 @@ type composeOptions struct {
|
||||
type upOptions struct {
|
||||
*composeOptions
|
||||
Detach bool
|
||||
Environment []string
|
||||
noStart bool
|
||||
noDeps bool
|
||||
cascadeStop bool
|
||||
@@ -50,6 +49,7 @@ type upOptions struct {
|
||||
noPrefix bool
|
||||
attachDependencies bool
|
||||
attach []string
|
||||
wait bool
|
||||
}
|
||||
|
||||
func (opts upOptions) apply(project *types.Project, services []string) error {
|
||||
@@ -98,30 +98,12 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
upCmd := &cobra.Command{
|
||||
Use: "up [SERVICE...]",
|
||||
Short: "Create and start containers",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
create.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if up.exitCodeFrom != "" {
|
||||
up.cascadeStop = true
|
||||
}
|
||||
if create.Build && create.noBuild {
|
||||
return fmt.Errorf("--build and --no-build are incompatible")
|
||||
}
|
||||
if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) {
|
||||
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
|
||||
}
|
||||
if create.forceRecreate && create.noRecreate {
|
||||
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
|
||||
}
|
||||
if create.recreateDeps && create.noRecreate {
|
||||
return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
|
||||
}
|
||||
return nil
|
||||
return validateFlags(&up, &create)
|
||||
}),
|
||||
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
|
||||
ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"]
|
||||
create.ignoreOrphans = strings.ToLower(ignore) == "true"
|
||||
create.ignoreOrphans = utils.StringToBool(project.Environment["COMPOSE_IGNORE_ORPHANS"])
|
||||
if create.ignoreOrphans && create.removeOrphans {
|
||||
return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined")
|
||||
}
|
||||
@@ -130,7 +112,6 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
}
|
||||
flags := upCmd.Flags()
|
||||
flags.StringArrayVarP(&up.Environment, "environment", "e", []string{}, "Environment variables")
|
||||
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
|
||||
flags.BoolVar(&create.Build, "build", false, "Build images before starting containers.")
|
||||
flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's missing.")
|
||||
@@ -150,10 +131,36 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.")
|
||||
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.")
|
||||
flags.StringArrayVar(&up.attach, "attach", []string{}, "Attach to service output.")
|
||||
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
|
||||
|
||||
return upCmd
|
||||
}
|
||||
|
||||
func validateFlags(up *upOptions, create *createOptions) error {
|
||||
if up.exitCodeFrom != "" {
|
||||
up.cascadeStop = true
|
||||
}
|
||||
if up.wait {
|
||||
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
|
||||
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
|
||||
}
|
||||
up.Detach = true
|
||||
}
|
||||
if create.Build && create.noBuild {
|
||||
return fmt.Errorf("--build and --no-build are incompatible")
|
||||
}
|
||||
if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) {
|
||||
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
|
||||
}
|
||||
if create.forceRecreate && create.noRecreate {
|
||||
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
|
||||
}
|
||||
if create.recreateDeps && create.noRecreate {
|
||||
return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, backend api.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error {
|
||||
if len(project.Services) == 0 {
|
||||
return fmt.Errorf("no service selected")
|
||||
@@ -201,6 +208,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions
|
||||
AttachTo: attachTo,
|
||||
ExitCodeFrom: upOptions.exitCodeFrom,
|
||||
CascadeStop: upOptions.cascadeStop,
|
||||
Wait: upOptions.wait,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
@@ -34,10 +35,9 @@ type versionOptions struct {
|
||||
func versionCommand() *cobra.Command {
|
||||
opts := versionOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show the Docker Compose version information",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Hidden: true,
|
||||
Use: "version",
|
||||
Short: "Show the Docker Compose version information",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
runVersion(opts)
|
||||
return nil
|
||||
@@ -53,7 +53,7 @@ func versionCommand() *cobra.Command {
|
||||
|
||||
func runVersion(opts versionOptions) {
|
||||
if opts.short {
|
||||
fmt.Println(internal.Version)
|
||||
fmt.Println(strings.TrimPrefix(internal.Version, "v"))
|
||||
return
|
||||
}
|
||||
if opts.format == formatter.JSON {
|
||||
|
||||
12
cmd/main.go
12
cmd/main.go
@@ -17,12 +17,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compatibility"
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -34,7 +37,7 @@ func init() {
|
||||
"To provide feedback or request new features please open issues at https://github.com/docker/compose"
|
||||
}
|
||||
|
||||
func main() {
|
||||
func pluginMain() {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
lazyInit := api.NewServiceProxy()
|
||||
cmd := commands.RootCommand(lazyInit)
|
||||
@@ -63,3 +66,10 @@ func main() {
|
||||
Version: internal.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
if commands.RunningAsStandalone() {
|
||||
os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
|
||||
}
|
||||
pluginMain()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ Builds, (re)creates, starts, and attaches to containers for a service.
|
||||
|
||||
Unless they are already running, this command also starts any linked services.
|
||||
|
||||
The `docker compose up` command aggregates the output of each container (liked `docker compose logs --follow` does).
|
||||
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
|
||||
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
|
||||
background and leaves them running.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ long: |-
|
||||
|
||||
Unless they are already running, this command also starts any linked services.
|
||||
|
||||
The `docker compose up` command aggregates the output of each container (liked `docker compose logs --follow` does).
|
||||
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
|
||||
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
|
||||
background and leaves them running.
|
||||
|
||||
|
||||
@@ -32,7 +32,16 @@ func generateCliYaml(opts *options) error {
|
||||
disableFlagsInUseLine(cmd)
|
||||
|
||||
cmd.DisableAutoGenTag = true
|
||||
return clidocstool.GenYamlTree(cmd, opts.target)
|
||||
tool, err := clidocstool.New(clidocstool.Options{
|
||||
Root: cmd,
|
||||
SourceDir: opts.source,
|
||||
TargetDir: opts.target,
|
||||
Plugin: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tool.GenYamlTree(cmd)
|
||||
}
|
||||
|
||||
func disableFlagsInUseLine(cmd *cobra.Command) {
|
||||
|
||||
143
go.mod
143
go.mod
@@ -1,47 +1,144 @@
|
||||
module github.com/docker/compose/v2
|
||||
|
||||
go 1.16
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.2.3
|
||||
github.com/buger/goterm v1.0.0
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/cnabio/cnab-to-oci v0.3.1-beta1
|
||||
github.com/compose-spec/compose-go v1.0.1
|
||||
github.com/containerd/console v1.0.2
|
||||
github.com/containerd/containerd v1.5.4
|
||||
github.com/compose-spec/compose-go v1.1.0
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.6.1
|
||||
github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e
|
||||
github.com/docker/buildx v0.5.2-0.20210422185057-908a856079fc
|
||||
github.com/docker/cli v20.10.7+incompatible
|
||||
github.com/docker/cli-docs-tool v0.1.1
|
||||
github.com/docker/buildx v0.7.1
|
||||
github.com/docker/cli v20.10.12+incompatible
|
||||
github.com/docker/cli-docs-tool v0.2.1
|
||||
github.com/docker/docker v20.10.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/kr/pty v1.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/moby/buildkit v0.8.2-0.20210401015549-df49b648c8bf
|
||||
github.com/moby/buildkit v0.9.1-0.20211019185819-8778943ac3da
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.0.1
|
||||
github.com/opencontainers/image-spec v1.0.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gotest.tools v2.2.0+incompatible
|
||||
gotest.tools/v3 v3.0.3
|
||||
k8s.io/client-go v0.21.0 // indirect
|
||||
gotest.tools/v3 v3.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cnabio/cnab-go v0.10.0-beta1 // indirect
|
||||
github.com/containerd/continuity v0.2.2 // indirect
|
||||
github.com/containerd/typeurl v1.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gogo/googleapis v1.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.13.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/pkcs11 v1.0.3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.5.0 // indirect
|
||||
github.com/moby/sys/signal v0.6.0 // indirect
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/qri-io/jsonpointer v0.1.0 // indirect
|
||||
github.com/qri-io/jsonschema v0.1.1 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/theupdateframework/notary v0.6.1 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20210818161904-4442383b5028 // indirect
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib v0.21.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.21.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.21.0 // indirect
|
||||
go.opentelemetry.io/otel v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel/internal/metric v0.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.21.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.3.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/apimachinery v0.22.5 // indirect
|
||||
k8s.io/client-go v0.22.5 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
// (for buildx)
|
||||
replace github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
|
||||
replace (
|
||||
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible
|
||||
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220121014307-40bb9831756f+incompatible
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.0.0-20210714055410-d010b05b4939
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/httptrace/otelhttptrace v0.0.0-20210714055410-d010b05b4939
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/otelhttp v0.0.0-20210714055410-d010b05b4939
|
||||
)
|
||||
|
||||
@@ -37,11 +37,11 @@ type Service interface {
|
||||
// Create executes the equivalent to a `compose create`
|
||||
Create(ctx context.Context, project *types.Project, opts CreateOptions) error
|
||||
// Start executes the equivalent to a `compose start`
|
||||
Start(ctx context.Context, project *types.Project, options StartOptions) error
|
||||
Start(ctx context.Context, projectName string, options StartOptions) error
|
||||
// Restart restarts containers
|
||||
Restart(ctx context.Context, project *types.Project, options RestartOptions) error
|
||||
Restart(ctx context.Context, projectName string, options RestartOptions) error
|
||||
// Stop executes the equivalent to a `compose stop`
|
||||
Stop(ctx context.Context, project *types.Project, options StopOptions) error
|
||||
Stop(ctx context.Context, projectName string, options StopOptions) error
|
||||
// Up executes the equivalent to a `compose up`
|
||||
Up(ctx context.Context, project *types.Project, options UpOptions) error
|
||||
// Down executes the equivalent to a `compose down`
|
||||
@@ -63,7 +63,7 @@ type Service interface {
|
||||
// Exec executes a command in a running service container
|
||||
Exec(ctx context.Context, project string, opts RunOptions) (int, error)
|
||||
// Copy copies a file/folder between a service container and the local filesystem
|
||||
Copy(ctx context.Context, project *types.Project, opts CopyOptions) error
|
||||
Copy(ctx context.Context, project string, options CopyOptions) error
|
||||
// Pause executes the equivalent to a `compose pause`
|
||||
Pause(ctx context.Context, project string, options PauseOptions) error
|
||||
// UnPause executes the equivalent to a `compose unpause`
|
||||
@@ -124,6 +124,8 @@ type StartOptions struct {
|
||||
CascadeStop bool
|
||||
// ExitCodeFrom return exit code from specified service
|
||||
ExitCodeFrom string
|
||||
// Wait won't return until containers reached the running|healthy state
|
||||
Wait bool
|
||||
}
|
||||
|
||||
// RestartOptions group options of the Restart API
|
||||
@@ -225,6 +227,8 @@ type RunOptions struct {
|
||||
Privileged bool
|
||||
UseNetworkAliases bool
|
||||
NoDeps bool
|
||||
// QuietPull makes the pulling process quiet
|
||||
QuietPull bool
|
||||
// used by exec
|
||||
Index int
|
||||
}
|
||||
@@ -400,10 +404,11 @@ const (
|
||||
|
||||
// Stack holds the name and state of a compose application/stack
|
||||
type Stack struct {
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Reason string
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
ConfigFiles string
|
||||
Reason string
|
||||
}
|
||||
|
||||
// LogConsumer is a callback to process log messages from services
|
||||
@@ -432,6 +437,8 @@ const (
|
||||
ContainerEventLog = iota
|
||||
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
|
||||
ContainerEventAttach
|
||||
// ContainerEventStopped is a ContainerEvent of type stopped.
|
||||
ContainerEventStopped
|
||||
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
|
||||
ContainerEventExit
|
||||
// UserCancel user cancelled compose up, we are stopping containers
|
||||
|
||||
@@ -28,9 +28,9 @@ type ServiceProxy struct {
|
||||
PushFn func(ctx context.Context, project *types.Project, options PushOptions) error
|
||||
PullFn func(ctx context.Context, project *types.Project, opts PullOptions) error
|
||||
CreateFn func(ctx context.Context, project *types.Project, opts CreateOptions) error
|
||||
StartFn func(ctx context.Context, project *types.Project, options StartOptions) error
|
||||
RestartFn func(ctx context.Context, project *types.Project, options RestartOptions) error
|
||||
StopFn func(ctx context.Context, project *types.Project, options StopOptions) error
|
||||
StartFn func(ctx context.Context, projectName string, options StartOptions) error
|
||||
RestartFn func(ctx context.Context, projectName string, options RestartOptions) error
|
||||
StopFn func(ctx context.Context, projectName string, options StopOptions) error
|
||||
UpFn func(ctx context.Context, project *types.Project, options UpOptions) error
|
||||
DownFn func(ctx context.Context, projectName string, options DownOptions) error
|
||||
LogsFn func(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error
|
||||
@@ -41,7 +41,7 @@ type ServiceProxy struct {
|
||||
RunOneOffContainerFn func(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
|
||||
RemoveFn func(ctx context.Context, project *types.Project, options RemoveOptions) error
|
||||
ExecFn func(ctx context.Context, project string, opts RunOptions) (int, error)
|
||||
CopyFn func(ctx context.Context, project *types.Project, opts CopyOptions) error
|
||||
CopyFn func(ctx context.Context, project string, options CopyOptions) error
|
||||
PauseFn func(ctx context.Context, project string, options PauseOptions) error
|
||||
UnPauseFn func(ctx context.Context, project string, options PauseOptions) error
|
||||
TopFn func(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error)
|
||||
@@ -141,36 +141,27 @@ func (s *ServiceProxy) Create(ctx context.Context, project *types.Project, optio
|
||||
}
|
||||
|
||||
// Start implements Service interface
|
||||
func (s *ServiceProxy) Start(ctx context.Context, project *types.Project, options StartOptions) error {
|
||||
func (s *ServiceProxy) Start(ctx context.Context, projectName string, options StartOptions) error {
|
||||
if s.StartFn == nil {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
for _, i := range s.interceptors {
|
||||
i(ctx, project)
|
||||
}
|
||||
return s.StartFn(ctx, project, options)
|
||||
return s.StartFn(ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Restart implements Service interface
|
||||
func (s *ServiceProxy) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
|
||||
func (s *ServiceProxy) Restart(ctx context.Context, projectName string, options RestartOptions) error {
|
||||
if s.RestartFn == nil {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
for _, i := range s.interceptors {
|
||||
i(ctx, project)
|
||||
}
|
||||
return s.RestartFn(ctx, project, options)
|
||||
return s.RestartFn(ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Stop implements Service interface
|
||||
func (s *ServiceProxy) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
|
||||
func (s *ServiceProxy) Stop(ctx context.Context, projectName string, options StopOptions) error {
|
||||
if s.StopFn == nil {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
for _, i := range s.interceptors {
|
||||
i(ctx, project)
|
||||
}
|
||||
return s.StopFn(ctx, project, options)
|
||||
return s.StopFn(ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Up implements Service interface
|
||||
@@ -269,13 +260,10 @@ func (s *ServiceProxy) Exec(ctx context.Context, project string, options RunOpti
|
||||
}
|
||||
|
||||
// Copy implements Service interface
|
||||
func (s *ServiceProxy) Copy(ctx context.Context, project *types.Project, options CopyOptions) error {
|
||||
func (s *ServiceProxy) Copy(ctx context.Context, project string, options CopyOptions) error {
|
||||
if s.CopyFn == nil {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
for _, i := range s.interceptors {
|
||||
i(ctx, project)
|
||||
}
|
||||
return s.CopyFn(ctx, project, options)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,16 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/driver"
|
||||
_ "github.com/docker/buildx/driver/docker" // required to get default driver registered
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
bclient "github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
@@ -69,7 +71,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
buildOptions.Pull = options.Pull
|
||||
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, args)
|
||||
buildOptions.NoCache = options.NoCache
|
||||
opts[imageName] = buildOptions
|
||||
buildOptions.CacheFrom, err = buildflags.ParseCacheEntry(service.Build.CacheFrom)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -81,6 +82,8 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
Attrs: map[string]string{"ref": image},
|
||||
})
|
||||
}
|
||||
|
||||
opts[imageName] = buildOptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
if project.Services[i].Labels == nil {
|
||||
project.Services[i].Labels = types.Labels{}
|
||||
}
|
||||
project.Services[i].Labels[api.ImageDigestLabel] = digest
|
||||
project.Services[i].CustomLabels[api.ImageDigestLabel] = digest
|
||||
project.Services[i].Image = image
|
||||
}
|
||||
}
|
||||
@@ -189,64 +192,31 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
|
||||
info, err := s.apiClient.Info(ctx)
|
||||
func (s *composeService) serverInfo(ctx context.Context) (command.ServerInfo, error) {
|
||||
ping, err := s.apiClient.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return command.ServerInfo{}, err
|
||||
}
|
||||
serverInfo := command.ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
}
|
||||
return serverInfo, err
|
||||
}
|
||||
|
||||
if info.OSType == "windows" {
|
||||
// no support yet for Windows container builds in Buildkit
|
||||
// https://docs.docker.com/develop/develop-images/build_enhancements/#limitations
|
||||
err := s.windowsBuild(opts, mode)
|
||||
return nil, WrapCategorisedComposeError(err, BuildFailure)
|
||||
}
|
||||
func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
|
||||
if len(opts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
const drivername = "default"
|
||||
|
||||
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, "", nil, nil, project.WorkingDir)
|
||||
serverInfo, err := s.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driverInfo := []build.DriverInfo{
|
||||
{
|
||||
Name: "default",
|
||||
Driver: d,
|
||||
},
|
||||
if buildkitEnabled, err := command.BuildKitEnabled(serverInfo); err != nil || !buildkitEnabled {
|
||||
return s.doBuildClassic(ctx, opts)
|
||||
}
|
||||
|
||||
// Progress needs its own context that lives longer than the
|
||||
// build one otherwise it won't read all the messages from
|
||||
// build and will lock
|
||||
progressCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
w := xprogress.NewPrinter(progressCtx, os.Stdout, mode)
|
||||
|
||||
// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
|
||||
response, err := build.Build(ctx, driverInfo, opts, nil, nil, w)
|
||||
errW := w.Wait()
|
||||
if err == nil {
|
||||
err = errW
|
||||
}
|
||||
if err != nil {
|
||||
return nil, WrapCategorisedComposeError(err, BuildFailure)
|
||||
}
|
||||
|
||||
imagesBuilt := map[string]string{}
|
||||
for name, img := range response {
|
||||
if img == nil || len(img.ExporterResponse) == 0 {
|
||||
continue
|
||||
}
|
||||
digest, ok := img.ExporterResponse["containerimage.digest"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
imagesBuilt[name] = digest
|
||||
}
|
||||
|
||||
return imagesBuilt, err
|
||||
return s.doBuildBuildkit(ctx, project, opts, mode)
|
||||
}
|
||||
|
||||
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) {
|
||||
@@ -259,6 +229,13 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
}))
|
||||
|
||||
var plats []specs.Platform
|
||||
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
|
||||
p, err := platforms.Parse(platform)
|
||||
if err != nil {
|
||||
return build.Options{}, err
|
||||
}
|
||||
plats = append(plats, p)
|
||||
}
|
||||
if service.Platform != "" {
|
||||
p, err := platforms.Parse(service.Platform)
|
||||
if err != nil {
|
||||
@@ -270,7 +247,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
return build.Options{
|
||||
Inputs: build.Inputs{
|
||||
ContextPath: service.Build.Context,
|
||||
DockerfilePath: service.Build.Dockerfile,
|
||||
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
|
||||
},
|
||||
BuildArgs: buildArgs,
|
||||
Tags: tags,
|
||||
@@ -309,3 +286,10 @@ func mergeArgs(m ...types.Mapping) types.Mapping {
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func dockerFilePath(context string, dockerfile string) string {
|
||||
if urlutil.IsGitURL(context) || filepath.IsAbs(dockerfile) {
|
||||
return dockerfile
|
||||
}
|
||||
return filepath.Join(context, dockerfile)
|
||||
}
|
||||
|
||||
73
pkg/compose/build_buildkit.go
Normal file
73
pkg/compose/build_buildkit.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/driver"
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
|
||||
const drivername = "default"
|
||||
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, nil, nil, nil, project.WorkingDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driverInfo := []build.DriverInfo{
|
||||
{
|
||||
Name: drivername,
|
||||
Driver: d,
|
||||
},
|
||||
}
|
||||
|
||||
// Progress needs its own context that lives longer than the
|
||||
// build one otherwise it won't read all the messages from
|
||||
// build and will lock
|
||||
progressCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
w := xprogress.NewPrinter(progressCtx, os.Stdout, mode)
|
||||
|
||||
// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
|
||||
response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile.Filename), w)
|
||||
errW := w.Wait()
|
||||
if err == nil {
|
||||
err = errW
|
||||
}
|
||||
if err != nil {
|
||||
return nil, WrapCategorisedComposeError(err, BuildFailure)
|
||||
}
|
||||
|
||||
imagesBuilt := map[string]string{}
|
||||
for name, img := range response {
|
||||
if img == nil || len(img.ExporterResponse) == 0 {
|
||||
continue
|
||||
}
|
||||
digest, ok := img.ExporterResponse["containerimage.digest"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
imagesBuilt[name] = digest
|
||||
}
|
||||
|
||||
return imagesBuilt, err
|
||||
}
|
||||
251
pkg/compose/build_classic.go
Normal file
251
pkg/compose/build_classic.go
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
buildx "github.com/docker/buildx/build"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *composeService) doBuildClassic(ctx context.Context, opts map[string]buildx.Options) (map[string]string, error) {
|
||||
var nameDigests = make(map[string]string)
|
||||
var errs error
|
||||
for name, o := range opts {
|
||||
digest, err := s.doBuildClassicSimpleImage(ctx, o)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err).ErrorOrNil()
|
||||
}
|
||||
nameDigests[name] = digest
|
||||
}
|
||||
|
||||
return nameDigests, errs
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options buildx.Options) (string, error) {
|
||||
var (
|
||||
buildCtx io.ReadCloser
|
||||
dockerfileCtx io.ReadCloser
|
||||
contextDir string
|
||||
tempDir string
|
||||
relDockerfile string
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
dockerfileName := options.Inputs.DockerfilePath
|
||||
specifiedContext := options.Inputs.ContextPath
|
||||
progBuff := os.Stdout
|
||||
buildBuff := os.Stdout
|
||||
if options.ImageIDFile != "" {
|
||||
// Avoid leaving a stale file if we eventually fail
|
||||
if err := os.Remove(options.ImageIDFile); err != nil && !os.IsNotExist(err) {
|
||||
return "", errors.Wrap(err, "removing image ID file")
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case isLocalDir(specifiedContext):
|
||||
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName)
|
||||
if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
|
||||
// Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx
|
||||
dockerfileCtx, err = os.Open(dockerfileName)
|
||||
if err != nil {
|
||||
return "", errors.Errorf("unable to open Dockerfile: %v", err)
|
||||
}
|
||||
defer dockerfileCtx.Close() // nolint:errcheck
|
||||
}
|
||||
case urlutil.IsGitURL(specifiedContext):
|
||||
tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, dockerfileName)
|
||||
case urlutil.IsURL(specifiedContext):
|
||||
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, dockerfileName)
|
||||
default:
|
||||
return "", errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Errorf("unable to prepare context: %s", err)
|
||||
}
|
||||
|
||||
if tempDir != "" {
|
||||
defer os.RemoveAll(tempDir) // nolint:errcheck
|
||||
contextDir = tempDir
|
||||
}
|
||||
|
||||
// read from a directory into tar archive
|
||||
if buildCtx == nil {
|
||||
excludes, err := build.ReadDockerignore(contextDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
|
||||
return "", errors.Wrap(err, "checking context")
|
||||
}
|
||||
|
||||
// And canonicalize dockerfile name to a platform-independent one
|
||||
relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
|
||||
|
||||
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, false)
|
||||
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||
ExcludePatterns: excludes,
|
||||
ChownOpts: &idtools.Identity{},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context
|
||||
if dockerfileCtx != nil && buildCtx != nil {
|
||||
buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
buildCtx, err = build.Compress(buildCtx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// if up to this point nothing has set the context then we must have another
|
||||
// way for sending it(streaming) and set the context to the Dockerfile
|
||||
if dockerfileCtx != nil && buildCtx == nil {
|
||||
buildCtx = dockerfileCtx
|
||||
}
|
||||
|
||||
progressOutput := streamformatter.NewProgressOutput(progBuff)
|
||||
var body io.Reader
|
||||
if buildCtx != nil {
|
||||
body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
|
||||
}
|
||||
|
||||
configFile := s.configFile
|
||||
creds, err := configFile.GetAllCredentials()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
authConfigs := make(map[string]dockertypes.AuthConfig, len(creds))
|
||||
for k, auth := range creds {
|
||||
authConfigs[k] = dockertypes.AuthConfig(auth)
|
||||
}
|
||||
buildOptions := imageBuildOptions(options)
|
||||
buildOptions.Version = dockertypes.BuilderV1
|
||||
buildOptions.Dockerfile = relDockerfile
|
||||
buildOptions.AuthConfigs = authConfigs
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
response, err := s.apiClient.ImageBuild(ctx, body, buildOptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close() // nolint:errcheck
|
||||
|
||||
imageID := ""
|
||||
aux := func(msg jsonmessage.JSONMessage) {
|
||||
var result dockertypes.BuildResult
|
||||
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse aux message: %s", err)
|
||||
} else {
|
||||
imageID = result.ID
|
||||
}
|
||||
}
|
||||
|
||||
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.Fd(), true, aux)
|
||||
if err != nil {
|
||||
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
||||
// If no error code is set, default to 1
|
||||
if jerr.Code == 0 {
|
||||
jerr.Code = 1
|
||||
}
|
||||
return "", cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Windows: show error message about modified file permissions if the
|
||||
// daemon isn't running Windows.
|
||||
if response.OSType != "windows" && runtime.GOOS == "windows" {
|
||||
// if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
|
||||
fmt.Fprintln(os.Stdout, "SECURITY WARNING: You are building a Docker "+
|
||||
"image from Windows against a non-Windows Docker host. All files and "+
|
||||
"directories added to build context will have '-rwxr-xr-x' permissions. "+
|
||||
"It is recommended to double check and reset permissions for sensitive "+
|
||||
"files and directories.")
|
||||
}
|
||||
|
||||
if options.ImageIDFile != "" {
|
||||
if imageID == "" {
|
||||
return "", errors.Errorf("Server did not provide an image ID. Cannot write %s", options.ImageIDFile)
|
||||
}
|
||||
if err := ioutil.WriteFile(options.ImageIDFile, []byte(imageID), 0666); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
func isLocalDir(c string) bool {
|
||||
_, err := os.Stat(c)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func imageBuildOptions(options buildx.Options) dockertypes.ImageBuildOptions {
|
||||
return dockertypes.ImageBuildOptions{
|
||||
Tags: options.Tags,
|
||||
NoCache: options.NoCache,
|
||||
Remove: true,
|
||||
PullParent: options.Pull,
|
||||
BuildArgs: toMapStringStringPtr(options.BuildArgs),
|
||||
Labels: options.Labels,
|
||||
NetworkMode: options.NetworkMode,
|
||||
ExtraHosts: options.ExtraHosts,
|
||||
Target: options.Target,
|
||||
}
|
||||
}
|
||||
|
||||
func toMapStringStringPtr(source map[string]string) map[string]*string {
|
||||
dest := make(map[string]*string)
|
||||
for k, v := range source {
|
||||
v := v
|
||||
dest[k] = &v
|
||||
}
|
||||
return dest
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/sanathkr/go-yaml"
|
||||
)
|
||||
|
||||
// Separator is used for naming components
|
||||
var Separator = "-"
|
||||
|
||||
// NewComposeService create a local implementation of the compose.Service API
|
||||
@@ -91,3 +93,59 @@ func escapeDollarSign(marshal []byte) []byte {
|
||||
escDollar := []byte{'$', '$'}
|
||||
return bytes.ReplaceAll(marshal, dollar, escDollar)
|
||||
}
|
||||
|
||||
// projectFromName builds a types.Project based on actual resources with compose labels set
|
||||
func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
|
||||
project := &types.Project{
|
||||
Name: projectName,
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return project, errors.New("no such project: " + projectName)
|
||||
}
|
||||
set := map[string]*types.ServiceConfig{}
|
||||
for _, c := range containers {
|
||||
serviceLabel := c.Labels[api.ServiceLabel]
|
||||
_, ok := set[serviceLabel]
|
||||
if !ok {
|
||||
set[serviceLabel] = &types.ServiceConfig{
|
||||
Name: serviceLabel,
|
||||
Image: c.Image,
|
||||
Labels: c.Labels,
|
||||
}
|
||||
}
|
||||
set[serviceLabel].Scale++
|
||||
}
|
||||
for _, service := range set {
|
||||
dependencies := service.Labels[api.DependenciesLabel]
|
||||
if len(dependencies) > 0 {
|
||||
service.DependsOn = types.DependsOnConfig{}
|
||||
for _, dc := range strings.Split(dependencies, ",") {
|
||||
dcArr := strings.Split(dc, ":")
|
||||
condition := ServiceConditionRunningOrHealthy
|
||||
dependency := dcArr[0]
|
||||
|
||||
// backward compatibility
|
||||
if len(dcArr) > 1 {
|
||||
condition = dcArr[1]
|
||||
}
|
||||
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition}
|
||||
}
|
||||
}
|
||||
project.Services = append(project.Services, *service)
|
||||
}
|
||||
SERVICES:
|
||||
for _, qs := range services {
|
||||
for _, es := range project.Services {
|
||||
if es.Name == qs {
|
||||
continue SERVICES
|
||||
}
|
||||
}
|
||||
return project, errors.New("no such service: " + qs)
|
||||
}
|
||||
err := project.ForServices(services)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@@ -86,6 +87,14 @@ func isNotOneOff(c moby.Container) bool {
|
||||
return !ok || v == "False"
|
||||
}
|
||||
|
||||
func indexed(index int) containerPredicate {
|
||||
return func(c moby.Container) bool {
|
||||
number := c.Labels[api.ContainerNumberLabel]
|
||||
idx, err := strconv.Atoi(number)
|
||||
return err == nil && index == idx
|
||||
}
|
||||
}
|
||||
|
||||
// filter return Containers with elements to match predicate
|
||||
func (containers Containers) filter(predicate containerPredicate) Containers {
|
||||
var filtered Containers
|
||||
|
||||
@@ -109,56 +109,61 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
|
||||
var mu sync.Mutex
|
||||
|
||||
// updateProject updates project after service converged, so dependent services relying on `service:xx` can refer to actual containers.
|
||||
func (c *convergence) updateProject(project *types.Project, service string) {
|
||||
containers := c.getObservedState(service)
|
||||
if len(containers) == 0 {
|
||||
return
|
||||
}
|
||||
container := containers[0]
|
||||
|
||||
func (c *convergence) updateProject(project *types.Project, serviceName string) {
|
||||
// operation is protected by a Mutex so that we can safely update project.Services while running concurrent convergence on services
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
cnts := c.getObservedState(serviceName)
|
||||
for i, s := range project.Services {
|
||||
if d := getDependentServiceFromMode(s.NetworkMode); d == service {
|
||||
s.NetworkMode = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(s.Ipc); d == service {
|
||||
s.Ipc = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(s.Pid); d == service {
|
||||
s.Pid = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
var links []string
|
||||
for _, serviceLink := range s.Links {
|
||||
parts := strings.Split(serviceLink, ":")
|
||||
serviceName := serviceLink
|
||||
serviceAlias := ""
|
||||
if len(parts) == 2 {
|
||||
serviceName = parts[0]
|
||||
serviceAlias = parts[1]
|
||||
}
|
||||
if serviceName != service {
|
||||
links = append(links, serviceLink)
|
||||
continue
|
||||
}
|
||||
for _, container := range containers {
|
||||
name := getCanonicalContainerName(container)
|
||||
if serviceAlias != "" {
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, serviceAlias))
|
||||
}
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, name),
|
||||
fmt.Sprintf("%s:%s", name, getContainerNameWithoutProject(container)))
|
||||
}
|
||||
s.Links = links
|
||||
}
|
||||
updateServices(&s, cnts)
|
||||
project.Services[i] = s
|
||||
}
|
||||
}
|
||||
|
||||
func updateServices(service *types.ServiceConfig, cnts Containers) {
|
||||
if len(cnts) == 0 {
|
||||
return
|
||||
}
|
||||
cnt := cnts[0]
|
||||
serviceName := cnt.Labels[api.ServiceLabel]
|
||||
|
||||
if d := getDependentServiceFromMode(service.NetworkMode); d == serviceName {
|
||||
service.NetworkMode = types.NetworkModeContainerPrefix + cnt.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(service.Ipc); d == serviceName {
|
||||
service.Ipc = types.NetworkModeContainerPrefix + cnt.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(service.Pid); d == serviceName {
|
||||
service.Pid = types.NetworkModeContainerPrefix + cnt.ID
|
||||
}
|
||||
var links []string
|
||||
for _, serviceLink := range service.Links {
|
||||
parts := strings.Split(serviceLink, ":")
|
||||
serviceName := serviceLink
|
||||
serviceAlias := ""
|
||||
if len(parts) == 2 {
|
||||
serviceName = parts[0]
|
||||
serviceAlias = parts[1]
|
||||
}
|
||||
if serviceName != service.Name {
|
||||
links = append(links, serviceLink)
|
||||
continue
|
||||
}
|
||||
for _, container := range cnts {
|
||||
name := getCanonicalContainerName(container)
|
||||
if serviceAlias != "" {
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, serviceAlias))
|
||||
}
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, name),
|
||||
fmt.Sprintf("%s:%s", name, getContainerNameWithoutProject(container)))
|
||||
}
|
||||
service.Links = links
|
||||
}
|
||||
}
|
||||
|
||||
func (c *convergence) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error {
|
||||
expected, err := getScale(service)
|
||||
if err != nil {
|
||||
@@ -256,9 +261,33 @@ func getContainerProgressName(container moby.Container) string {
|
||||
return "Container " + getCanonicalContainerName(container)
|
||||
}
|
||||
|
||||
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
|
||||
func containerEvents(containers Containers, eventFunc func(string) progress.Event) []progress.Event {
|
||||
events := []progress.Event{}
|
||||
for _, container := range containers {
|
||||
events = append(events, eventFunc(getContainerProgressName(container)))
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// ServiceConditionRunningOrHealthy is a service condition on statys running or healthy
|
||||
const ServiceConditionRunningOrHealthy = "running_or_healthy"
|
||||
|
||||
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependencies types.DependsOnConfig) error {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
for dep, config := range service.DependsOn {
|
||||
w := progress.ContextWriter(ctx)
|
||||
for dep, config := range dependencies {
|
||||
if shouldWait, err := shouldWaitForDependency(dep, config, project); err != nil {
|
||||
return err
|
||||
} else if !shouldWait {
|
||||
continue
|
||||
}
|
||||
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, dep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Events(containerEvents(containers, progress.Waiting))
|
||||
|
||||
dep, config := dep, config
|
||||
eg.Go(func() error {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
@@ -266,12 +295,22 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
for {
|
||||
<-ticker.C
|
||||
switch config.Condition {
|
||||
case types.ServiceConditionHealthy:
|
||||
healthy, err := s.isServiceHealthy(ctx, project, dep)
|
||||
case ServiceConditionRunningOrHealthy:
|
||||
healthy, err := s.isServiceHealthy(ctx, project, dep, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if healthy {
|
||||
w.Events(containerEvents(containers, progress.Healthy))
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionHealthy:
|
||||
healthy, err := s.isServiceHealthy(ctx, project, dep, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if healthy {
|
||||
w.Events(containerEvents(containers, progress.Healthy))
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionCompletedSuccessfully:
|
||||
@@ -280,14 +319,12 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
if exited {
|
||||
w.Events(containerEvents(containers, progress.Exited))
|
||||
if code != 0 {
|
||||
return fmt.Errorf("service %q didn't completed successfully: exit %d", dep, code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionStarted:
|
||||
// already managed by InDependencyOrder
|
||||
return nil
|
||||
default:
|
||||
logrus.Warnf("unsupported depends_on condition: %s", config.Condition)
|
||||
return nil
|
||||
@@ -298,6 +335,20 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceDependency, project *types.Project) (bool, error) {
|
||||
if dependencyConfig.Condition == types.ServiceConditionStarted {
|
||||
// already managed by InDependencyOrder
|
||||
return false, nil
|
||||
}
|
||||
if service, err := project.GetService(serviceName); err != nil {
|
||||
return false, err
|
||||
} else if service.Scale == 0 {
|
||||
// don't wait for the dependency which configured to have 0 containers running
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func nextContainerNumber(containers []moby.Container) (int, error) {
|
||||
max := 0
|
||||
for _, c := range containers {
|
||||
@@ -433,7 +484,10 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
||||
Networks: inspectedContainer.NetworkSettings.Networks,
|
||||
},
|
||||
}
|
||||
links := append(service.Links, service.ExternalLinks...)
|
||||
links, err := s.getLinks(ctx, project.Name, service, number)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
for _, netName := range service.NetworksByPriority() {
|
||||
netwrk := project.Networks[netName]
|
||||
cfg := service.Networks[netName]
|
||||
@@ -461,6 +515,64 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
||||
return created, err
|
||||
}
|
||||
|
||||
// getLinks mimics V1 compose/service.py::Service::_get_links()
|
||||
func (s composeService) getLinks(ctx context.Context, projectName string, service types.ServiceConfig, number int) ([]string, error) {
|
||||
var links []string
|
||||
format := func(k, v string) string {
|
||||
return fmt.Sprintf("%s:%s", k, v)
|
||||
}
|
||||
getServiceContainers := func(serviceName string) (Containers, error) {
|
||||
return s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
|
||||
}
|
||||
|
||||
for _, rawLink := range service.Links {
|
||||
linkSplit := strings.Split(rawLink, ":")
|
||||
linkServiceName := linkSplit[0]
|
||||
linkName := linkServiceName
|
||||
if len(linkSplit) == 2 {
|
||||
linkName = linkSplit[1] // linkName if informed like in: "serviceName:linkName"
|
||||
}
|
||||
cnts, err := getServiceContainers(linkServiceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range cnts {
|
||||
containerName := getCanonicalContainerName(c)
|
||||
links = append(links,
|
||||
format(containerName, linkName),
|
||||
format(containerName, strings.Join([]string{linkServiceName, strconv.Itoa(number)}, Separator)),
|
||||
format(containerName, strings.Join([]string{projectName, linkServiceName, strconv.Itoa(number)}, Separator)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if service.Labels[api.OneoffLabel] == "True" {
|
||||
cnts, err := getServiceContainers(service.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range cnts {
|
||||
containerName := getCanonicalContainerName(c)
|
||||
links = append(links,
|
||||
format(containerName, service.Name),
|
||||
format(containerName, strings.TrimPrefix(containerName, projectName+Separator)),
|
||||
format(containerName, containerName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, rawExtLink := range service.ExternalLinks {
|
||||
extLinkSplit := strings.Split(rawExtLink, ":")
|
||||
externalLink := extLinkSplit[0]
|
||||
linkName := externalLink
|
||||
if len(extLinkSplit) == 2 {
|
||||
linkName = extLinkSplit[1]
|
||||
}
|
||||
links = append(links, format(externalLink, linkName))
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func shortIDAliasExists(containerID string, aliases ...string) bool {
|
||||
for _, alias := range aliases {
|
||||
if alias == containerID[:12] {
|
||||
@@ -497,7 +609,7 @@ func (s *composeService) connectContainerToNetwork(ctx context.Context, id strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Project, service string) (bool, error) {
|
||||
func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Project, service string, fallbackRunning bool) (bool, error) {
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, service)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -511,11 +623,23 @@ func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Pr
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if container.Config.Healthcheck == nil && fallbackRunning {
|
||||
// Container does not define a health check, but we can fall back to "running" state
|
||||
return container.State != nil && container.State.Status == "running", nil
|
||||
}
|
||||
|
||||
if container.State == nil || container.State.Health == nil {
|
||||
return false, fmt.Errorf("container for service %q has no healthcheck configured", service)
|
||||
}
|
||||
if container.State.Health.Status != moby.Healthy {
|
||||
switch container.State.Health.Status {
|
||||
case moby.Healthy:
|
||||
// Continue by checking the next container.
|
||||
case moby.Unhealthy:
|
||||
return false, fmt.Errorf("container for service %q is unhealthy", service)
|
||||
case moby.Starting:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("container for service %q had unexpected health status %q", service, container.State.Health.Status)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
@@ -539,7 +663,11 @@ func (s *composeService) isServiceCompleted(ctx context.Context, project *types.
|
||||
}
|
||||
|
||||
func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
|
||||
err := s.waitDependencies(ctx, project, service)
|
||||
if service.Deploy != nil && service.Deploy.Replicas != nil && *service.Deploy.Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := s.waitDependencies(ctx, project, service.DependsOn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -559,7 +687,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
|
||||
if scale, err := getScale(service); err != nil && scale == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no containers to start")
|
||||
return fmt.Errorf("service %q has no container to start", service.Name)
|
||||
}
|
||||
|
||||
w := progress.ContextWriter(ctx)
|
||||
|
||||
@@ -17,10 +17,17 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/golang/mock/gomock"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
@@ -46,3 +53,163 @@ func TestContainerName(t *testing.T) {
|
||||
_, err = getScale(s)
|
||||
assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName))
|
||||
}
|
||||
|
||||
func TestServiceLinks(t *testing.T) {
|
||||
const dbContainerName = "/" + testProject + "-db-1"
|
||||
const webContainerName = "/" + testProject + "-web-1"
|
||||
s := types.ServiceConfig{
|
||||
Name: "web",
|
||||
Scale: 1,
|
||||
}
|
||||
|
||||
containerListOptions := moby.ContainerListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(testProject),
|
||||
serviceFilter("db"),
|
||||
oneOffFilter(false),
|
||||
),
|
||||
All: true,
|
||||
}
|
||||
|
||||
t.Run("service links default", func(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = apiClient
|
||||
|
||||
s.Links = []string{"db"}
|
||||
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil)
|
||||
|
||||
links, err := tested.getLinks(context.Background(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
assert.Equal(t, links[0], "testProject-db-1:db")
|
||||
assert.Equal(t, links[1], "testProject-db-1:db-1")
|
||||
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
|
||||
})
|
||||
|
||||
t.Run("service links", func(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = apiClient
|
||||
|
||||
s.Links = []string{"db:db"}
|
||||
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil)
|
||||
links, err := tested.getLinks(context.Background(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
assert.Equal(t, links[0], "testProject-db-1:db")
|
||||
assert.Equal(t, links[1], "testProject-db-1:db-1")
|
||||
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
|
||||
})
|
||||
|
||||
t.Run("service links name", func(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = apiClient
|
||||
|
||||
s.Links = []string{"db:dbname"}
|
||||
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil)
|
||||
|
||||
links, err := tested.getLinks(context.Background(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
assert.Equal(t, links[0], "testProject-db-1:dbname")
|
||||
assert.Equal(t, links[1], "testProject-db-1:db-1")
|
||||
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
|
||||
})
|
||||
|
||||
t.Run("service links external links", func(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = apiClient
|
||||
|
||||
s.Links = []string{"db:dbname"}
|
||||
s.ExternalLinks = []string{"db1:db2"}
|
||||
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil)
|
||||
|
||||
links, err := tested.getLinks(context.Background(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 4)
|
||||
assert.Equal(t, links[0], "testProject-db-1:dbname")
|
||||
assert.Equal(t, links[1], "testProject-db-1:db-1")
|
||||
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
|
||||
|
||||
// ExternalLink
|
||||
assert.Equal(t, links[3], "db1:db2")
|
||||
})
|
||||
|
||||
t.Run("service links itself oneoff", func(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = apiClient
|
||||
|
||||
s.Links = []string{}
|
||||
s.ExternalLinks = []string{}
|
||||
s.Labels = s.Labels.Add(api.OneoffLabel, "True")
|
||||
|
||||
c := testContainer("web", webContainerName, true)
|
||||
containerListOptionsOneOff := moby.ContainerListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(testProject),
|
||||
serviceFilter("web"),
|
||||
oneOffFilter(false),
|
||||
),
|
||||
All: true,
|
||||
}
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptionsOneOff).Return([]moby.Container{c}, nil)
|
||||
|
||||
links, err := tested.getLinks(context.Background(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
assert.Equal(t, links[0], "testProject-web-1:web")
|
||||
assert.Equal(t, links[1], "testProject-web-1:web-1")
|
||||
assert.Equal(t, links[2], "testProject-web-1:testProject-web-1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWaitDependencies(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
tested.apiClient = api
|
||||
|
||||
t.Run("should skip dependencies with scale 0", func(t *testing.T) {
|
||||
dbService := types.ServiceConfig{Name: "db", Scale: 0}
|
||||
redisService := types.ServiceConfig{Name: "redis", Scale: 0}
|
||||
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{dbService, redisService}}
|
||||
dependencies := types.DependsOnConfig{
|
||||
"db": {Condition: ServiceConditionRunningOrHealthy},
|
||||
"redis": {Condition: ServiceConditionRunningOrHealthy},
|
||||
}
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, dependencies))
|
||||
})
|
||||
t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
|
||||
dbService := types.ServiceConfig{Name: "db", Scale: 1}
|
||||
redisService := types.ServiceConfig{Name: "redis", Scale: 1}
|
||||
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{dbService, redisService}}
|
||||
dependencies := types.DependsOnConfig{
|
||||
"db": {Condition: types.ServiceConditionStarted},
|
||||
"redis": {Condition: types.ServiceConditionStarted},
|
||||
}
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, dependencies))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,11 +26,9 @@ import (
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/pkg/errors"
|
||||
@@ -44,7 +42,7 @@ const (
|
||||
acrossServices = fromService | toService
|
||||
)
|
||||
|
||||
func (s *composeService) Copy(ctx context.Context, project *types.Project, opts api.CopyOptions) error {
|
||||
func (s *composeService) Copy(ctx context.Context, project string, opts api.CopyOptions) error {
|
||||
srcService, srcPath := splitCpArg(opts.Source)
|
||||
destService, dstPath := splitCpArg(opts.Destination)
|
||||
|
||||
@@ -64,20 +62,17 @@ func (s *composeService) Copy(ctx context.Context, project *types.Project, opts
|
||||
serviceName = destService
|
||||
}
|
||||
|
||||
f := filters.NewArgs(
|
||||
projectFilter(project.Name),
|
||||
serviceFilter(serviceName),
|
||||
)
|
||||
if !opts.All {
|
||||
f.Add("label", fmt.Sprintf("%s=%d", api.ContainerNumberLabel, opts.Index))
|
||||
}
|
||||
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{Filters: f})
|
||||
containers, err := s.getContainers(ctx, project, oneOffExclude, true, serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(containers) < 1 {
|
||||
return fmt.Errorf("service %s not running", serviceName)
|
||||
return fmt.Errorf("no container found for service %q", serviceName)
|
||||
}
|
||||
|
||||
if !opts.All {
|
||||
containers = containers.filter(indexed(opts.Index))
|
||||
}
|
||||
|
||||
g := errgroup.Group{}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
|
||||
volume.Labels = volume.Labels.Add(api.VolumeLabel, k)
|
||||
volume.Labels = volume.Labels.Add(api.ProjectLabel, project.Name)
|
||||
volume.Labels = volume.Labels.Add(api.VersionLabel, api.ComposeVersion)
|
||||
err := s.ensureVolume(ctx, volume)
|
||||
err := s.ensureVolume(ctx, volume, project.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func getImageName(service types.ServiceConfig, projectName string) string {
|
||||
func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig,
|
||||
number int, inherit *moby.Container, autoRemove bool, attachStdin bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
|
||||
|
||||
labels, err := s.prepareLabels(p, service, number)
|
||||
labels, err := s.prepareLabels(service, number)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -378,7 +378,9 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
|
||||
PidMode: container.PidMode(service.Pid),
|
||||
Tmpfs: tmpfs,
|
||||
Isolation: container.Isolation(service.Isolation),
|
||||
Runtime: service.Runtime,
|
||||
LogConfig: logConfig,
|
||||
GroupAdd: service.GroupAdd,
|
||||
}
|
||||
|
||||
return &containerConfig, &hostConfig, networkConfig, nil
|
||||
@@ -412,45 +414,42 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, error
|
||||
return securityOpts, nil
|
||||
}
|
||||
|
||||
func (s *composeService) prepareLabels(p *types.Project, service types.ServiceConfig, number int) (map[string]string, error) {
|
||||
func (s *composeService) prepareLabels(service types.ServiceConfig, number int) (map[string]string, error) {
|
||||
labels := map[string]string{}
|
||||
for k, v := range service.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
labels[api.ProjectLabel] = p.Name
|
||||
labels[api.ServiceLabel] = service.Name
|
||||
labels[api.VersionLabel] = api.ComposeVersion
|
||||
if _, ok := service.Labels[api.OneoffLabel]; !ok {
|
||||
labels[api.OneoffLabel] = "False"
|
||||
for k, v := range service.CustomLabels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
hash, err := ServiceHash(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels[api.ConfigHashLabel] = hash
|
||||
labels[api.WorkingDirLabel] = p.WorkingDir
|
||||
labels[api.ConfigFilesLabel] = strings.Join(p.ComposeFiles, ",")
|
||||
|
||||
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
|
||||
|
||||
var dependencies []string
|
||||
for s := range service.DependsOn {
|
||||
dependencies = append(dependencies, s)
|
||||
for s, d := range service.DependsOn {
|
||||
dependencies = append(dependencies, s+":"+d.Condition)
|
||||
}
|
||||
labels[api.DependenciesLabel] = strings.Join(dependencies, ",")
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string {
|
||||
mode := "none"
|
||||
if len(project.Networks) > 0 {
|
||||
for name := range getNetworksForService(service) {
|
||||
mode = project.Networks[name].Name
|
||||
break
|
||||
}
|
||||
if len(project.Networks) == 0 {
|
||||
return "none"
|
||||
}
|
||||
return mode
|
||||
|
||||
if len(service.Networks) > 0 {
|
||||
name := service.NetworksByPriority()[0]
|
||||
return project.Networks[name].Name
|
||||
}
|
||||
|
||||
return project.Networks["default"].Name
|
||||
}
|
||||
|
||||
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
|
||||
@@ -492,6 +491,7 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
|
||||
MemorySwap: int64(s.MemSwapLimit),
|
||||
MemorySwappiness: swappiness,
|
||||
MemoryReservation: int64(s.MemReservation),
|
||||
OomKillDisable: &s.OomKillDisable,
|
||||
CPUCount: s.CPUCount,
|
||||
CPUPeriod: s.CPUPeriod,
|
||||
CPUQuota: s.CPUQuota,
|
||||
@@ -502,6 +502,10 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
|
||||
CpusetCpus: s.CPUSet,
|
||||
}
|
||||
|
||||
if s.PidsLimit != 0 {
|
||||
resources.PidsLimit = &s.PidsLimit
|
||||
}
|
||||
|
||||
setBlkio(s.BlkioConfig, &resources)
|
||||
|
||||
if s.Deploy != nil {
|
||||
@@ -525,6 +529,9 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
|
||||
case 1:
|
||||
src = arr[0]
|
||||
}
|
||||
if dst == "" {
|
||||
dst = src
|
||||
}
|
||||
resources.Devices = append(resources.Devices, container.DeviceMapping{
|
||||
PathOnHost: src,
|
||||
PathInContainer: dst,
|
||||
@@ -631,15 +638,11 @@ func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap {
|
||||
bindings := nat.PortMap{}
|
||||
for _, port := range s.Ports {
|
||||
p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
|
||||
bind := bindings[p]
|
||||
binding := nat.PortBinding{
|
||||
HostIP: port.HostIP,
|
||||
HostIP: port.HostIP,
|
||||
HostPort: port.Published,
|
||||
}
|
||||
if port.Published > 0 {
|
||||
binding.HostPort = fmt.Sprint(port.Published)
|
||||
}
|
||||
bind = append(bind, binding)
|
||||
bindings[p] = bind
|
||||
bindings[p] = append(bindings[p], binding)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
@@ -709,11 +712,7 @@ MOUNTS:
|
||||
if m.Type == mount.TypeBind || m.Type == mount.TypeNamedPipe {
|
||||
for _, v := range service.Volumes {
|
||||
if v.Target == m.Target && v.Bind != nil && v.Bind.CreateHostPath {
|
||||
mode := "rw"
|
||||
if m.ReadOnly {
|
||||
mode = "ro"
|
||||
}
|
||||
binds = append(binds, fmt.Sprintf("%s:%s:%s", m.Source, m.Target, mode))
|
||||
binds = append(binds, fmt.Sprintf("%s:%s:%s", m.Source, m.Target, getBindMode(v.Bind, m.ReadOnly)))
|
||||
continue MOUNTS
|
||||
}
|
||||
}
|
||||
@@ -723,6 +722,23 @@ MOUNTS:
|
||||
return volumeMounts, binds, mounts, nil
|
||||
}
|
||||
|
||||
func getBindMode(bind *types.ServiceVolumeBind, readOnly bool) string {
|
||||
mode := "rw"
|
||||
|
||||
if readOnly {
|
||||
mode = "ro"
|
||||
}
|
||||
|
||||
switch bind.SELinux {
|
||||
case types.SELinuxShared:
|
||||
mode += ",z"
|
||||
case types.SELinuxPrivate:
|
||||
mode += ",Z"
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
|
||||
var mounts = map[string]mount.Mount{}
|
||||
if inherit != nil {
|
||||
@@ -747,24 +763,21 @@ func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, v := range s.Volumes {
|
||||
if v.Target != m.Destination {
|
||||
volumes := []types.ServiceVolumeConfig{}
|
||||
for _, v := range s.Volumes {
|
||||
if v.Target != m.Destination || v.Source != "" {
|
||||
volumes = append(volumes, v)
|
||||
continue
|
||||
}
|
||||
if v.Source == "" {
|
||||
// inherit previous container's anonymous volume
|
||||
mounts[m.Destination] = mount.Mount{
|
||||
Type: m.Type,
|
||||
Source: src,
|
||||
Target: m.Destination,
|
||||
ReadOnly: !m.RW,
|
||||
}
|
||||
// Avoid mount to be later re-defined
|
||||
l := len(s.Volumes) - 1
|
||||
s.Volumes[i] = s.Volumes[l]
|
||||
s.Volumes = s.Volumes[:l]
|
||||
// inherit previous container's anonymous volume
|
||||
mounts[m.Destination] = mount.Mount{
|
||||
Type: m.Type,
|
||||
Source: src,
|
||||
Target: m.Destination,
|
||||
ReadOnly: !m.RW,
|
||||
}
|
||||
}
|
||||
s.Volumes = volumes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,8 +959,8 @@ func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *m
|
||||
if volume.Bind != nil {
|
||||
logrus.Warnf("mount of type `tmpfs` should not define `bind` option")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
logrus.Warnf("mount of type `tmpfs` should not define `volumeZ` option")
|
||||
if volume.Volume != nil {
|
||||
logrus.Warnf("mount of type `tmpfs` should not define `volume` option")
|
||||
}
|
||||
return nil, nil, buildTmpfsOptions(volume.Tmpfs)
|
||||
}
|
||||
@@ -980,7 +993,7 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
|
||||
return nil
|
||||
}
|
||||
return &mount.TmpfsOptions{
|
||||
SizeBytes: tmpfs.Size,
|
||||
SizeBytes: int64(tmpfs.Size),
|
||||
// Mode: , // FIXME missing from model ?
|
||||
}
|
||||
}
|
||||
@@ -993,21 +1006,19 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
|
||||
return aliases
|
||||
}
|
||||
|
||||
func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
|
||||
if len(s.Networks) > 0 {
|
||||
return s.Networks
|
||||
}
|
||||
if s.NetworkMode != "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]*types.ServiceNetworkConfig{"default": nil}
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
|
||||
_, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
if n.External.External {
|
||||
if n.Driver == "overlay" {
|
||||
// Swarm nodes do not register overlay networks that were
|
||||
// created on a different node unless they're in use.
|
||||
// Here we assume `driver` is relevant for a network we don't manage
|
||||
// which is a non-sense, but this is our legacy ¯\(ツ)/¯
|
||||
// networkAttach will later fail anyway if network actually doesn't exists
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
|
||||
}
|
||||
var ipam *network.IPAM
|
||||
@@ -1034,6 +1045,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
|
||||
Internal: n.Internal,
|
||||
Attachable: n.Attachable,
|
||||
IPAM: ipam,
|
||||
EnableIPv6: n.EnableIPv6,
|
||||
}
|
||||
|
||||
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
|
||||
@@ -1079,27 +1091,48 @@ func (s *composeService) removeNetwork(ctx context.Context, networkID string, ne
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
|
||||
// TODO could identify volume by label vs name
|
||||
_, err := s.apiClient.VolumeInspect(ctx, volume.Name)
|
||||
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
|
||||
inspected, err := s.apiClient.VolumeInspect(ctx, volume.Name)
|
||||
if err != nil {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
eventName := fmt.Sprintf("Volume %q", volume.Name)
|
||||
w := progress.ContextWriter(ctx)
|
||||
w.Event(progress.CreatingEvent(eventName))
|
||||
_, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{
|
||||
Labels: volume.Labels,
|
||||
Name: volume.Name,
|
||||
Driver: volume.Driver,
|
||||
DriverOpts: volume.DriverOpts,
|
||||
})
|
||||
if err != nil {
|
||||
w.Event(progress.ErrorEvent(eventName))
|
||||
return err
|
||||
if volume.External.External {
|
||||
return fmt.Errorf("external volume %q not found", volume.Name)
|
||||
}
|
||||
w.Event(progress.CreatedEvent(eventName))
|
||||
err := s.createVolume(ctx, volume)
|
||||
return err
|
||||
}
|
||||
|
||||
if volume.External.External {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Volume exists with name, but let's double-check this is the expected one
|
||||
p, ok := inspected.Labels[api.ProjectLabel]
|
||||
if !ok {
|
||||
logrus.Warnf("volume %q already exists but was not created by Docker Compose. Use `external: true` to use an existing volume", volume.Name)
|
||||
}
|
||||
if ok && p != project {
|
||||
logrus.Warnf("volume %q already exists but was not created for project %q. Use `external: true` to use an existing volume", volume.Name, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
|
||||
eventName := fmt.Sprintf("Volume %q", volume.Name)
|
||||
w := progress.ContextWriter(ctx)
|
||||
w.Event(progress.CreatingEvent(eventName))
|
||||
_, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{
|
||||
Labels: volume.Labels,
|
||||
Name: volume.Name,
|
||||
Driver: volume.Driver,
|
||||
DriverOpts: volume.DriverOpts,
|
||||
})
|
||||
if err != nil {
|
||||
w.Event(progress.ErrorEvent(eventName))
|
||||
return err
|
||||
}
|
||||
w.Event(progress.CreatedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ package compose
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
composetypes "github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
mountTypes "github.com/docker/docker/api/types/mount"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
@@ -81,3 +82,143 @@ func TestPrepareNetworkLabels(t *testing.T) {
|
||||
"com.docker.compose.version": api.ComposeVersion,
|
||||
}))
|
||||
}
|
||||
|
||||
func TestBuildContainerMountOptions(t *testing.T) {
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
{
|
||||
Name: "myService",
|
||||
Volumes: []composetypes.ServiceVolumeConfig{
|
||||
{
|
||||
Type: composetypes.VolumeTypeVolume,
|
||||
Target: "/var/myvolume1",
|
||||
},
|
||||
{
|
||||
Type: composetypes.VolumeTypeVolume,
|
||||
Target: "/var/myvolume2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{
|
||||
"myVolume1": {
|
||||
Name: "myProject_myVolume1",
|
||||
},
|
||||
"myVolume2": {
|
||||
Name: "myProject_myVolume2",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
inherit := &moby.Container{
|
||||
Mounts: []moby.MountPoint{
|
||||
{
|
||||
Type: composetypes.VolumeTypeVolume,
|
||||
Destination: "/var/myvolume1",
|
||||
},
|
||||
{
|
||||
Type: composetypes.VolumeTypeVolume,
|
||||
Destination: "/var/myvolume2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mounts, err := buildContainerMountOptions(project, project.Services[0], moby.ImageInspect{}, inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, len(mounts) == 2)
|
||||
assert.Equal(t, mounts[0].Target, "/var/myvolume1")
|
||||
assert.Equal(t, mounts[1].Target, "/var/myvolume2")
|
||||
|
||||
mounts, err = buildContainerMountOptions(project, project.Services[0], moby.ImageInspect{}, inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, len(mounts) == 2)
|
||||
assert.Equal(t, mounts[0].Target, "/var/myvolume1")
|
||||
assert.Equal(t, mounts[1].Target, "/var/myvolume2")
|
||||
}
|
||||
|
||||
func TestGetBindMode(t *testing.T) {
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{}, false), "rw")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{}, true), "ro")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, false), "rw,z")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, false), "rw,Z")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, true), "ro,z")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, true), "ro,Z")
|
||||
}
|
||||
|
||||
func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
Networks: map[string]*composetypes.ServiceNetworkConfig{
|
||||
"myNetwork1": {
|
||||
Priority: 10,
|
||||
},
|
||||
"myNetwork2": {
|
||||
Priority: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
|
||||
"myNetwork1": {
|
||||
Name: "myProject_myNetwork1",
|
||||
},
|
||||
"myNetwork2": {
|
||||
Name: "myProject_myNetwork2",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2")
|
||||
})
|
||||
|
||||
t.Run("returns default network when service has no networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
|
||||
"myNetwork1": {
|
||||
Name: "myProject_myNetwork1",
|
||||
},
|
||||
"myNetwork2": {
|
||||
Name: "myProject_myNetwork2",
|
||||
},
|
||||
"default": {
|
||||
Name: "myProject_default",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default")
|
||||
})
|
||||
|
||||
t.Run("returns none if project has no networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "none")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex,
|
||||
for _, node := range nodes {
|
||||
// Don't start this service yet if all of its children have
|
||||
// not been started yet.
|
||||
if len(traversalConfig.filterAdjacentByStatusFn(graph, node.Service, traversalConfig.adjacentServiceStatusToSkip)) != 0 {
|
||||
if len(traversalConfig.filterAdjacentByStatusFn(graph, node.Key, traversalConfig.adjacentServiceStatusToSkip)) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex,
|
||||
return err
|
||||
}
|
||||
|
||||
graph.UpdateStatus(node.Service, traversalConfig.targetServiceStatus)
|
||||
graph.UpdateStatus(node.Key, traversalConfig.targetServiceStatus)
|
||||
|
||||
return run(ctx, graph, eg, traversalConfig.adjacentNodesFn(node), traversalConfig, fn)
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
|
||||
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
|
||||
builtFromResources := options.Project == nil
|
||||
w := progress.ContextWriter(ctx)
|
||||
resourceToRemove := false
|
||||
|
||||
@@ -50,8 +51,11 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Project == nil {
|
||||
options.Project = s.projectFromContainerLabels(containers.filter(isNotOneOff), projectName)
|
||||
if builtFromResources {
|
||||
options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) > 0 {
|
||||
@@ -75,7 +79,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
}
|
||||
|
||||
ops, err := s.ensureNetwoksDown(ctx, projectName)
|
||||
ops, err := s.ensureNetworksDown(ctx, projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,11 +89,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
|
||||
if options.Volumes {
|
||||
rm, err := s.ensureVolumesDown(ctx, projectName, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ops = append(ops, rm...)
|
||||
ops = append(ops, s.ensureVolumesDown(ctx, options.Project, w)...)
|
||||
}
|
||||
|
||||
if !resourceToRemove && len(ops) == 0 {
|
||||
@@ -103,19 +103,15 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) ensureVolumesDown(ctx context.Context, projectName string, w progress.Writer) ([]downOp, error) {
|
||||
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
|
||||
var ops []downOp
|
||||
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
|
||||
if err != nil {
|
||||
return ops, err
|
||||
}
|
||||
for _, vol := range volumes.Volumes {
|
||||
id := vol.Name
|
||||
for _, vol := range project.Volumes {
|
||||
volumeName := vol.Name
|
||||
ops = append(ops, func() error {
|
||||
return s.removeVolume(ctx, id, w)
|
||||
return s.removeVolume(ctx, volumeName, w)
|
||||
})
|
||||
}
|
||||
return ops, nil
|
||||
return ops
|
||||
}
|
||||
|
||||
func (s *composeService) ensureImagesDown(ctx context.Context, projectName string, options api.DownOptions, w progress.Writer) []downOp {
|
||||
@@ -129,7 +125,7 @@ func (s *composeService) ensureImagesDown(ctx context.Context, projectName strin
|
||||
return ops
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetwoksDown(ctx context.Context, projectName string) ([]downOp, error) {
|
||||
func (s *composeService) ensureNetworksDown(ctx context.Context, projectName string) ([]downOp, error) {
|
||||
var ops []downOp
|
||||
networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))})
|
||||
if err != nil {
|
||||
@@ -237,31 +233,21 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) projectFromContainerLabels(containers Containers, projectName string) *types.Project {
|
||||
project := &types.Project{
|
||||
Name: projectName,
|
||||
func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
|
||||
containers = containers.filter(isNotOneOff)
|
||||
project, _ := s.projectFromName(containers, projectName)
|
||||
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return project
|
||||
}
|
||||
set := map[string]moby.Container{}
|
||||
for _, c := range containers {
|
||||
set[c.Labels[api.ServiceLabel]] = c
|
||||
}
|
||||
for s, c := range set {
|
||||
service := types.ServiceConfig{
|
||||
Name: s,
|
||||
Image: c.Image,
|
||||
Labels: c.Labels,
|
||||
|
||||
project.Volumes = types.Volumes{}
|
||||
for _, vol := range volumes.Volumes {
|
||||
project.Volumes[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
|
||||
Name: vol.Name,
|
||||
Driver: vol.Driver,
|
||||
Labels: vol.Labels,
|
||||
}
|
||||
dependencies := c.Labels[api.DependenciesLabel]
|
||||
if len(dependencies) > 0 {
|
||||
service.DependsOn = types.DependsOnConfig{}
|
||||
for _, d := range strings.Split(dependencies, ",") {
|
||||
service.DependsOn[d] = types.ServiceDependency{}
|
||||
}
|
||||
}
|
||||
project.Services = append(project.Services, service)
|
||||
}
|
||||
return project
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ func TestDown(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "456", nil).Return(nil)
|
||||
@@ -74,6 +76,8 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil)
|
||||
@@ -100,13 +104,16 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
|
||||
[]moby.Container{testContainer("service1", "123", false)}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{
|
||||
Volumes: []*moby.Volume{{Name: "myProject_volume"}},
|
||||
}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(nil)
|
||||
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).Return(nil, nil)
|
||||
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).Return(volume.VolumeListOKBody{Volumes: []*moby.Volume{{Name: "myProject_volume"}}}, nil)
|
||||
api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil)
|
||||
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
|
||||
|
||||
@@ -90,13 +90,13 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
|
||||
|
||||
stdout := ContainerStdout{HijackedResponse: resp}
|
||||
stdin := ContainerStdin{HijackedResponse: resp}
|
||||
r, err := s.getEscapeKeyProxy(opts.Stdin)
|
||||
r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in := streams.NewIn(opts.Stdin)
|
||||
if in.IsTerminal() {
|
||||
if in.IsTerminal() && opts.Tty {
|
||||
state, err := term.SetRawTerminal(in.FD())
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
)
|
||||
|
||||
// ServiceHash compute configuration has for a service
|
||||
// TODO move this to compose-go
|
||||
func ServiceHash(o types.ServiceConfig) (string, error) {
|
||||
// remove the Build config when generating the service hash
|
||||
o.Build = nil
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
|
||||
allContainers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(projectFilter(projectName)),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s *composeService) kill(ctx context.Context, project *types.Project, optio
|
||||
}
|
||||
|
||||
var containers Containers
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffInclude, true, services...)
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffInclude, false, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ func TestKillAll(t *testing.T) {
|
||||
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{testService("service1"), testService("service2")}}
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, projectFilterListOpt()).Return(
|
||||
api.EXPECT().ContainerList(ctx, moby.ContainerListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).Return(
|
||||
[]moby.Container{testContainer("service1", "123", false), testContainer("service1", "456", false), testContainer("service2", "789", false)}, nil)
|
||||
api.EXPECT().ContainerKill(anyCancellableContext(), "123", "").Return(nil)
|
||||
api.EXPECT().ContainerKill(anyCancellableContext(), "456", "").Return(nil)
|
||||
@@ -64,9 +66,7 @@ func TestKillSignal(t *testing.T) {
|
||||
|
||||
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{testService(serviceName)}}
|
||||
listOptions := moby.ContainerListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)),
|
||||
serviceFilter(serviceName)),
|
||||
All: true,
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)), serviceFilter(serviceName)),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -34,21 +34,43 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
if options.Follow {
|
||||
eg.Go(func() error {
|
||||
printer := newLogPrinter(consumer)
|
||||
return s.watchContainers(ctx, projectName, options.Services, printer.HandleEvent, containers, func(c types.Container) error {
|
||||
return s.logContainers(ctx, consumer, c, options)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
c := c
|
||||
eg.Go(func() error {
|
||||
return s.logContainers(ctx, consumer, c, options)
|
||||
})
|
||||
}
|
||||
|
||||
if options.Follow {
|
||||
printer := newLogPrinter(consumer)
|
||||
eg.Go(func() error {
|
||||
for _, c := range containers {
|
||||
printer.HandleEvent(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(c),
|
||||
Service: c.Labels[api.ServiceLabel],
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
return s.watchContainers(ctx, projectName, options.Services, printer.HandleEvent, containers, func(c types.Container) error {
|
||||
printer.HandleEvent(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(c),
|
||||
Service: c.Labels[api.ServiceLabel],
|
||||
})
|
||||
return s.logContainers(ctx, consumer, c, options)
|
||||
})
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
_, err := printer.Run(ctx, false, "", nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@@ -46,15 +48,40 @@ func containersToStacks(containers []moby.Container) ([]api.Stack, error) {
|
||||
}
|
||||
var projects []api.Stack
|
||||
for _, project := range keys {
|
||||
configFiles, err := combinedConfigFiles(containersByLabel[project])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects = append(projects, api.Stack{
|
||||
ID: project,
|
||||
Name: project,
|
||||
Status: combinedStatus(containerToState(containersByLabel[project])),
|
||||
ID: project,
|
||||
Name: project,
|
||||
Status: combinedStatus(containerToState(containersByLabel[project])),
|
||||
ConfigFiles: configFiles,
|
||||
})
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func combinedConfigFiles(containers []moby.Container) (string, error) {
|
||||
configFiles := []string{}
|
||||
|
||||
for _, c := range containers {
|
||||
files, ok := c.Labels[api.ConfigFilesLabel]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, c.ID)
|
||||
}
|
||||
|
||||
for _, f := range strings.Split(files, ",") {
|
||||
if !utils.StringContains(configFiles, f) {
|
||||
configFiles = append(configFiles, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(configFiles, ","), nil
|
||||
}
|
||||
|
||||
func containerToState(containers []moby.Container) []string {
|
||||
statuses := []string{}
|
||||
for _, c := range containers {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -30,31 +31,33 @@ func TestContainersToStacks(t *testing.T) {
|
||||
{
|
||||
ID: "service1",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project1"},
|
||||
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
|
||||
},
|
||||
{
|
||||
ID: "service2",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project1"},
|
||||
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
|
||||
},
|
||||
{
|
||||
ID: "service3",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project2"},
|
||||
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
|
||||
},
|
||||
}
|
||||
stacks, err := containersToStacks(containers)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, stacks, []api.Stack{
|
||||
{
|
||||
ID: "project1",
|
||||
Name: "project1",
|
||||
Status: "running(2)",
|
||||
ID: "project1",
|
||||
Name: "project1",
|
||||
Status: "running(2)",
|
||||
ConfigFiles: "/home/docker-compose.yaml",
|
||||
},
|
||||
{
|
||||
ID: "project2",
|
||||
Name: "project2",
|
||||
Status: "running(1)",
|
||||
ID: "project2",
|
||||
Name: "project2",
|
||||
Status: "running(1)",
|
||||
ConfigFiles: "/home/project2-docker-compose.yaml",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -64,3 +67,56 @@ func TestStacksMixedStatus(t *testing.T) {
|
||||
assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
|
||||
assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
|
||||
}
|
||||
|
||||
func TestCombinedConfigFiles(t *testing.T) {
|
||||
containersByLabel := map[string][]moby.Container{
|
||||
"project1": {
|
||||
{
|
||||
ID: "service1",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
|
||||
},
|
||||
{
|
||||
ID: "service2",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
|
||||
},
|
||||
},
|
||||
"project2": {
|
||||
{
|
||||
ID: "service3",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
|
||||
},
|
||||
},
|
||||
"project3": {
|
||||
{
|
||||
ID: "service4",
|
||||
State: "running",
|
||||
Labels: map[string]string{api.ProjectLabel: "project3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testData := map[string]struct {
|
||||
ConfigFiles string
|
||||
Error error
|
||||
}{
|
||||
"project1": {ConfigFiles: "/home/docker-compose.yaml", Error: nil},
|
||||
"project2": {ConfigFiles: "/home/project2-docker-compose.yaml", Error: nil},
|
||||
"project3": {ConfigFiles: "", Error: fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, "service4")},
|
||||
}
|
||||
|
||||
for project, containers := range containersByLabel {
|
||||
configFiles, err := combinedConfigFiles(containers)
|
||||
|
||||
expected := testData[project]
|
||||
|
||||
if expected.Error != nil {
|
||||
assert.Equal(t, err.Error(), expected.Error.Error())
|
||||
} else {
|
||||
assert.Equal(t, err, expected.Error)
|
||||
}
|
||||
assert.Equal(t, configFiles, expected.ConfigFiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (s *composeService) Pause(ctx context.Context, project string, options api.
|
||||
}
|
||||
|
||||
func (s *composeService) pause(ctx context.Context, project string, options api.PauseOptions) error {
|
||||
containers, err := s.getContainers(ctx, project, oneOffExclude, true, options.Services...)
|
||||
containers, err := s.getContainers(ctx, project, oneOffExclude, false, options.Services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (s *composeService) UnPause(ctx context.Context, project string, options ap
|
||||
}
|
||||
|
||||
func (s *composeService) unPause(ctx context.Context, project string, options api.PauseOptions) error {
|
||||
containers, err := s.getContainers(ctx, project, oneOffExclude, true, options.Services...)
|
||||
containers, err := s.getContainers(ctx, project, oneOffExclude, false, options.Services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
// logPrinter watch application containers an collect their logs
|
||||
type logPrinter interface {
|
||||
HandleEvent(event api.ContainerEvent)
|
||||
Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error)
|
||||
Run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error)
|
||||
Cancel()
|
||||
}
|
||||
|
||||
@@ -56,56 +57,61 @@ func (p *printer) HandleEvent(event api.ContainerEvent) {
|
||||
p.queue <- event
|
||||
}
|
||||
|
||||
func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) {
|
||||
//nolint:gocyclo
|
||||
func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) {
|
||||
var (
|
||||
aborting bool
|
||||
exitCode int
|
||||
)
|
||||
containers := map[string]struct{}{}
|
||||
for {
|
||||
event := <-p.queue
|
||||
container := event.Container
|
||||
switch event.Type {
|
||||
case api.UserCancel:
|
||||
aborting = true
|
||||
case api.ContainerEventAttach:
|
||||
if _, ok := containers[container]; ok {
|
||||
continue
|
||||
}
|
||||
containers[container] = struct{}{}
|
||||
p.consumer.Register(container)
|
||||
case api.ContainerEventExit:
|
||||
if !event.Restarting {
|
||||
delete(containers, container)
|
||||
}
|
||||
if !aborting {
|
||||
p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode))
|
||||
}
|
||||
if cascadeStop {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return exitCode, ctx.Err()
|
||||
case event := <-p.queue:
|
||||
container := event.Container
|
||||
switch event.Type {
|
||||
case api.UserCancel:
|
||||
aborting = true
|
||||
case api.ContainerEventAttach:
|
||||
if _, ok := containers[container]; ok {
|
||||
continue
|
||||
}
|
||||
containers[container] = struct{}{}
|
||||
p.consumer.Register(container)
|
||||
case api.ContainerEventExit, api.ContainerEventStopped:
|
||||
if !event.Restarting {
|
||||
delete(containers, container)
|
||||
}
|
||||
if !aborting {
|
||||
aborting = true
|
||||
fmt.Println("Aborting on container exit...")
|
||||
err := stopFn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode))
|
||||
}
|
||||
if cascadeStop {
|
||||
if !aborting {
|
||||
aborting = true
|
||||
fmt.Println("Aborting on container exit...")
|
||||
err := stopFn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if exitCodeFrom == "" {
|
||||
exitCodeFrom = event.Service
|
||||
}
|
||||
if exitCodeFrom == event.Service {
|
||||
logrus.Error(event.ExitCode)
|
||||
exitCode = event.ExitCode
|
||||
}
|
||||
}
|
||||
if exitCodeFrom == "" {
|
||||
exitCodeFrom = event.Service
|
||||
if len(containers) == 0 {
|
||||
// Last container terminated, done
|
||||
return exitCode, nil
|
||||
}
|
||||
if exitCodeFrom == event.Service {
|
||||
logrus.Error(event.ExitCode)
|
||||
exitCode = event.ExitCode
|
||||
case api.ContainerEventLog:
|
||||
if !aborting {
|
||||
p.consumer.Log(container, event.Service, event.Line)
|
||||
}
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
// Last container terminated, done
|
||||
return exitCode, nil
|
||||
}
|
||||
case api.ContainerEventLog:
|
||||
if !aborting {
|
||||
p.consumer.Log(container, event.Service, event.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@@ -27,14 +26,20 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
|
||||
func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.restart(ctx, project, options)
|
||||
return s.restart(ctx, projectName, options)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *composeService) restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
|
||||
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
|
||||
func (s *composeService) restart(ctx context.Context, projectName string, options api.RestartOptions) error {
|
||||
|
||||
observedState, err := s.getContainers(ctx, projectName, oneOffInclude, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := s.projectFromName(observedState, projectName, options.Services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
||||
}
|
||||
|
||||
func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
|
||||
r, err := s.getEscapeKeyProxy(opts.Stdin)
|
||||
r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (s *composeService) runInteractive(ctx context.Context, containerID string,
|
||||
}
|
||||
|
||||
in := streams.NewIn(opts.Stdin)
|
||||
if in.IsTerminal() {
|
||||
if in.IsTerminal() && opts.Tty {
|
||||
state, err := term.SetRawTerminal(in.FD())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -153,17 +153,25 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
if service.Deploy != nil {
|
||||
service.Deploy.RestartPolicy = nil
|
||||
}
|
||||
service.Labels = service.Labels.Add(api.SlugLabel, slug)
|
||||
service.Labels = service.Labels.Add(api.OneoffLabel, "True")
|
||||
service.CustomLabels = service.CustomLabels.
|
||||
Add(api.SlugLabel, slug).
|
||||
Add(api.OneoffLabel, "True")
|
||||
|
||||
if err := s.ensureImagesExists(ctx, project, false); err != nil { // all dependencies already checked, but might miss service img
|
||||
if err := s.ensureImagesExists(ctx, project, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
|
||||
return "", err
|
||||
}
|
||||
if !opts.NoDeps {
|
||||
if err := s.waitDependencies(ctx, project, service); err != nil {
|
||||
if err := s.waitDependencies(ctx, project, service.DependsOn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
updateServices(&service, observedState)
|
||||
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -172,7 +180,10 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
return containerID, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getEscapeKeyProxy(r io.ReadCloser) (io.ReadCloser, error) {
|
||||
func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.ReadCloser, error) {
|
||||
if !isTty {
|
||||
return r, nil
|
||||
}
|
||||
var escapeKeys = []byte{16, 17}
|
||||
if s.configFile.DetachKeys != "" {
|
||||
customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys)
|
||||
|
||||
@@ -28,15 +28,22 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Start(ctx context.Context, project *types.Project, options api.StartOptions) error {
|
||||
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.start(ctx, project, options, nil)
|
||||
return s.start(ctx, projectName, options, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *composeService) start(ctx context.Context, project *types.Project, options api.StartOptions, listener api.ContainerEventListener) error {
|
||||
if len(options.AttachTo) == 0 {
|
||||
options.AttachTo = project.ServiceNames()
|
||||
func (s *composeService) start(ctx context.Context, projectName string, options api.StartOptions, listener api.ContainerEventListener) error {
|
||||
var containers Containers
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := s.projectFromName(containers, projectName, options.AttachTo...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
@@ -53,16 +60,31 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti
|
||||
})
|
||||
}
|
||||
|
||||
err := InDependencyOrder(ctx, project, func(c context.Context, name string) error {
|
||||
err = InDependencyOrder(ctx, project, func(c context.Context, name string) error {
|
||||
service, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.startService(ctx, project, service)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Wait {
|
||||
depends := types.DependsOnConfig{}
|
||||
for _, s := range project.Services {
|
||||
depends[s.Name] = types.ServiceDependency{
|
||||
Condition: ServiceConditionRunningOrHealthy,
|
||||
}
|
||||
}
|
||||
err = s.waitDependencies(ctx, project, depends)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
@@ -79,6 +101,12 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string
|
||||
err := s.Events(ctx, projectName, api.EventsOptions{
|
||||
Services: services,
|
||||
Consumer: func(event api.Event) error {
|
||||
if event.Status == "destroy" {
|
||||
// This container can't be inspected, because it's gone.
|
||||
// It's already been removed from the watched map.
|
||||
return nil
|
||||
}
|
||||
|
||||
inspected, err := s.apiClient.ContainerInspect(ctx, event.Container)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -90,11 +118,26 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string
|
||||
}
|
||||
name := getContainerNameWithoutProject(container)
|
||||
|
||||
if event.Status == "stop" {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventStopped,
|
||||
Container: name,
|
||||
Service: container.Labels[api.ServiceLabel],
|
||||
})
|
||||
|
||||
delete(watched, container.ID)
|
||||
if len(watched) == 0 {
|
||||
// all project containers stopped, we're done
|
||||
stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if event.Status == "die" {
|
||||
restarted := watched[container.ID]
|
||||
watched[container.ID] = restarted + 1
|
||||
// Container terminated.
|
||||
willRestart := inspected.HostConfig.RestartPolicy.MaximumRetryCount > restarted
|
||||
willRestart := willContainerRestart(inspected, restarted)
|
||||
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventExit,
|
||||
@@ -141,3 +184,14 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func willContainerRestart(container moby.ContainerJSON, restarted int) bool {
|
||||
policy := container.HostConfig.RestartPolicy
|
||||
if policy.IsAlways() || policy.IsUnlessStopped() {
|
||||
return true
|
||||
}
|
||||
if policy.IsOnFailure() {
|
||||
return container.State.ExitCode != 0 && policy.MaximumRetryCount > restarted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,25 +21,29 @@ import (
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
func (s *composeService) Stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
|
||||
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.stop(ctx, project, options)
|
||||
return s.stop(ctx, projectName, options)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *composeService) stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
|
||||
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
w := progress.ContextWriter(ctx)
|
||||
|
||||
services := options.Services
|
||||
if len(services) == 0 {
|
||||
services = project.ServiceNames()
|
||||
services = []string{}
|
||||
}
|
||||
|
||||
var containers Containers
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffInclude, true, services...)
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffInclude, true, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := s.projectFromName(containers, projectName, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
compose "github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/golang/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -50,13 +49,7 @@ func TestStopTimeout(t *testing.T) {
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "456", &timeout).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "789", &timeout).Return(nil)
|
||||
|
||||
err := tested.Stop(ctx, &types.Project{
|
||||
Name: strings.ToLower(testProject),
|
||||
Services: []types.ServiceConfig{
|
||||
{Name: "service1"},
|
||||
{Name: "service2"},
|
||||
},
|
||||
}, compose.StopOptions{
|
||||
err := tested.Stop(ctx, strings.ToLower(testProject), compose.StopOptions{
|
||||
Timeout: &timeout,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
return err
|
||||
}
|
||||
if options.Start.Attach == nil {
|
||||
return s.start(ctx, project, options.Start, nil)
|
||||
return s.start(ctx, project.Name, options.Start, nil)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -65,7 +65,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
})
|
||||
}()
|
||||
|
||||
return s.Stop(ctx, project, api.StopOptions{
|
||||
return s.Stop(ctx, project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
})
|
||||
})
|
||||
@@ -80,12 +80,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
var exitCode int
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
code, err := printer.Run(options.Start.CascadeStop, options.Start.ExitCodeFrom, stopFunc)
|
||||
code, err := printer.Run(context.Background(), options.Start.CascadeStop, options.Start.ExitCodeFrom, stopFunc)
|
||||
exitCode = code
|
||||
return err
|
||||
})
|
||||
|
||||
err = s.start(ctx, project, options.Start, printer.HandleEvent)
|
||||
err = s.start(ctx, project.Name, options.Start, printer.HandleEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestComposeCancel(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
t.Run("metrics on cancel Compose build", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "ls")
|
||||
c.RunDockerComposeCmd("ls")
|
||||
buildProjectPath := "fixtures/build-infinite/compose.yaml"
|
||||
|
||||
// require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal.
|
||||
|
||||
@@ -45,6 +45,6 @@ func TestCascadeStop(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -37,7 +34,7 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
c.RunDockerOrExitError("rmi", "build-test_nginx")
|
||||
c.RunDockerOrExitError("rmi", "custom-nginx")
|
||||
|
||||
res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "build")
|
||||
res := c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "build")
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
c.RunDockerCmd("image", "inspect", "build-test_nginx")
|
||||
@@ -49,7 +46,7 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
c.RunDockerOrExitError("rmi", "build-test_nginx")
|
||||
c.RunDockerOrExitError("rmi", "custom-nginx")
|
||||
|
||||
c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "build", "--build-arg", "FOO=BAR")
|
||||
c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "build", "--build-arg", "FOO=BAR")
|
||||
|
||||
res := c.RunDockerCmd("image", "inspect", "build-test_nginx")
|
||||
res.Assert(t, icmd.Expected{Out: `"FOO": "BAR"`})
|
||||
@@ -69,13 +66,26 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{Out: `"FOO": "BAR"`})
|
||||
})
|
||||
|
||||
t.Run("build with multiple build-args ", func(t *testing.T) {
|
||||
// ensure local test run does not reuse previously build image
|
||||
c.RunDockerOrExitError("rmi", "-f", "multi-args_multiargs")
|
||||
cmd := c.NewDockerCmd("compose", "--project-directory", "fixtures/build-test/multi-args", "build")
|
||||
|
||||
icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
|
||||
cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
|
||||
})
|
||||
|
||||
res := c.RunDockerCmd("image", "inspect", "multi-args_multiargs")
|
||||
res.Assert(t, icmd.Expected{Out: `"RESULT": "SUCCESS"`})
|
||||
})
|
||||
|
||||
t.Run("build as part of up", func(t *testing.T) {
|
||||
c.RunDockerOrExitError("rmi", "build-test_nginx")
|
||||
c.RunDockerOrExitError("rmi", "custom-nginx")
|
||||
|
||||
res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
res := c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "up", "-d")
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "down")
|
||||
c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "down")
|
||||
})
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
@@ -89,59 +99,21 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("no rebuild when up again", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
res := c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "up", "-d")
|
||||
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("rebuild when up --build", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d", "--build")
|
||||
res := c.RunDockerComposeCmd("--workdir", "fixtures/build-test", "up", "-d", "--build")
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
})
|
||||
|
||||
t.Run("cleanup build project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "down")
|
||||
c.RunDockerComposeCmd("--project-directory", "fixtures/build-test", "down")
|
||||
c.RunDockerCmd("rmi", "build-test_nginx")
|
||||
c.RunDockerCmd("rmi", "custom-nginx")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalComposeBuildStaticDockerfilePath(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
t.Run("build ddev-style compose files", func(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "ddev")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(dir) //nolint:errcheck
|
||||
|
||||
assert.NilError(t, ioutil.WriteFile(filepath.Join(dir, "compose.yaml"), []byte(`services:
|
||||
service1:
|
||||
build:
|
||||
context: `+dir+`/service1
|
||||
dockerfile: Dockerfile
|
||||
service2:
|
||||
build:
|
||||
context: `+dir+`/service2
|
||||
dockerfile: `+dir+`/service2/Dockerfile
|
||||
`), 0644))
|
||||
|
||||
assert.NilError(t, os.Mkdir(filepath.Join(dir, "service1"), 0700))
|
||||
assert.NilError(t, ioutil.WriteFile(filepath.Join(dir, "service1", "Dockerfile"), []byte(`FROM alpine
|
||||
RUN echo "hello"
|
||||
`), 0644))
|
||||
|
||||
assert.NilError(t, os.Mkdir(filepath.Join(dir, "service2"), 0700))
|
||||
assert.NilError(t, ioutil.WriteFile(filepath.Join(dir, "service2", "Dockerfile"), []byte(`FROM alpine
|
||||
RUN echo "world"
|
||||
`), 0644))
|
||||
|
||||
res := c.RunDockerCmd("compose", "-f", filepath.Join(dir, "compose.yaml"), "build")
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: `RUN echo "hello"`})
|
||||
res.Assert(t, icmd.Expected{Out: `RUN echo "world"`})
|
||||
|
||||
c.RunDockerCmd("compose", "-f", filepath.Join(dir, "compose.yaml"), "down", "--rmi", "all")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestLocalComposeExec(t *testing.T) {
|
||||
|
||||
const projectName = "compose-e2e-exec"
|
||||
|
||||
c.RunDockerCmd("compose", "--project-directory", "fixtures/simple-composefile", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("--project-directory", "fixtures/simple-composefile", "--project-name", projectName, "up", "-d")
|
||||
|
||||
t.Run("exec true", func(t *testing.T) {
|
||||
res := c.RunDockerOrExitError("exec", "compose-e2e-exec-simple-1", "/bin/true")
|
||||
|
||||
@@ -29,11 +29,11 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
t.Run("compose run", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "back")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back")
|
||||
lines := Lines(res.Stdout())
|
||||
assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
|
||||
res = c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello one more time")
|
||||
res = c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello one more time")
|
||||
lines = Lines(res.Stdout())
|
||||
assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout())
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
|
||||
@@ -68,7 +68,7 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("compose run --rm", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo", "Hello again")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo", "Hello again")
|
||||
lines := Lines(res.Stdout())
|
||||
assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "down")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "down")
|
||||
res := c.RunDockerCmd("ps", "--all")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
|
||||
})
|
||||
@@ -85,7 +85,7 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
t.Run("compose run --volumes", func(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
|
||||
res.Assert(t, icmd.Expected{Out: "compose_run_test.go"})
|
||||
|
||||
res = c.RunDockerCmd("ps", "--all")
|
||||
@@ -93,13 +93,13 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("compose run --publish", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
|
||||
res := c.RunDockerCmd("ps")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "down")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "down")
|
||||
res := c.RunDockerCmd("ps", "--all")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
|
||||
})
|
||||
|
||||
@@ -33,22 +33,17 @@ import (
|
||||
|
||||
var binDir string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
exitCode := m.Run()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestLocalComposeUp(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
const projectName = "compose-e2e-demo"
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("check accessing running app", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `web`})
|
||||
|
||||
endpoint := "http://localhost:90"
|
||||
@@ -60,7 +55,7 @@ func TestLocalComposeUp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("top", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "top")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "top")
|
||||
output := res.Stdout()
|
||||
head := []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}
|
||||
for _, h := range head {
|
||||
@@ -98,21 +93,21 @@ func TestLocalComposeUp(t *testing.T) {
|
||||
StdoutContains(`"Name":"compose-e2e-demo-web-1","Command":"/dispatcher","Project":"compose-e2e-demo","Service":"web","State":"running","Health":"healthy"`),
|
||||
5*time.Second, 1*time.Second)
|
||||
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `NAME COMMAND SERVICE STATUS PORTS`})
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-web-1 "/dispatcher" web running (healthy) 0.0.0.0:90->80/tcp, :::90->80/tcp`})
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-db-1 "docker-entrypoint.s…" db running 5432/tcp`})
|
||||
})
|
||||
|
||||
t.Run("images", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "images")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "images")
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-db-1 gtardif/sentences-db latest`})
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-web-1 gtardif/sentences-web latest`})
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-words-1 gtardif/sentences-api latest`})
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
|
||||
t.Run("check containers after down", func(t *testing.T) {
|
||||
@@ -137,7 +132,6 @@ func TestComposePull(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDownComposefileInParentFolder(t *testing.T) {
|
||||
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
tmpFolder, err := ioutil.TempDir("fixtures/simple-composefile", "test-tmp")
|
||||
@@ -145,10 +139,10 @@ func TestDownComposefileInParentFolder(t *testing.T) {
|
||||
defer os.Remove(tmpFolder) // nolint: errcheck
|
||||
projectName := filepath.Base(tmpFolder)
|
||||
|
||||
res := c.RunDockerCmd("compose", "--project-directory", tmpFolder, "up", "-d")
|
||||
res := c.RunDockerComposeCmd("--project-directory", tmpFolder, "up", "-d")
|
||||
res.Assert(t, icmd.Expected{Err: "Started", ExitCode: 0})
|
||||
|
||||
res = c.RunDockerCmd("compose", "-p", projectName, "down")
|
||||
res = c.RunDockerComposeCmd("-p", projectName, "down")
|
||||
res.Assert(t, icmd.Expected{Err: "Removed", ExitCode: 0})
|
||||
}
|
||||
|
||||
@@ -181,11 +175,11 @@ func TestRm(t *testing.T) {
|
||||
const projectName = "compose-e2e-rm"
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("rm -sf", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm", "-sf", "simple")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm", "-sf", "simple")
|
||||
res.Assert(t, icmd.Expected{Err: "Removed", ExitCode: 0})
|
||||
})
|
||||
|
||||
@@ -195,7 +189,7 @@ func TestRm(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-p", projectName, "down")
|
||||
c.RunDockerComposeCmd("-p", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,7 +199,7 @@ func TestCompatibility(t *testing.T) {
|
||||
const projectName = "compose-e2e-compatibility"
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "--compatibility", "-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("--compatibility", "-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("check container names", func(t *testing.T) {
|
||||
@@ -216,6 +210,28 @@ func TestCompatibility(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-p", projectName, "down")
|
||||
c.RunDockerComposeCmd("-p", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
const projectName = "compose-e2e-convert"
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "convert")
|
||||
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services:
|
||||
nginx:
|
||||
build:
|
||||
context: %s
|
||||
dockerfile: Dockerfile
|
||||
networks:
|
||||
default: null
|
||||
networks:
|
||||
default:
|
||||
name: compose-e2e-convert_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestCopy(t *testing.T) {
|
||||
const projectName = "copy_e2e"
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "down")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "down")
|
||||
|
||||
os.Remove("./fixtures/cp-test/from-default.txt") //nolint:errcheck
|
||||
os.Remove("./fixtures/cp-test/from-indexed.txt") //nolint:errcheck
|
||||
@@ -39,16 +39,16 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("start service", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "up", "--scale", "nginx=5", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "up", "--scale", "nginx=5", "-d")
|
||||
})
|
||||
|
||||
t.Run("make sure service is running", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `nginx running`})
|
||||
})
|
||||
|
||||
t.Run("copy to container copies the file to the first container by default", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/default.txt")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/default.txt")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
output := c.RunDockerCmd("exec", projectName+"-nginx-1", "cat", "/tmp/default.txt").Stdout()
|
||||
@@ -59,7 +59,7 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("copy to container with a given index copies the file to the given container", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/indexed.txt")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/indexed.txt")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
output := c.RunDockerCmd("exec", projectName+"-nginx-3", "cat", "/tmp/indexed.txt").Stdout()
|
||||
@@ -70,7 +70,7 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("copy to container with the all flag copies the file to all containers", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--all", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/all.txt")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--all", "./fixtures/cp-test/cp-me.txt", "nginx:/tmp/all.txt")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
output := c.RunDockerCmd("exec", projectName+"-nginx-1", "cat", "/tmp/all.txt").Stdout()
|
||||
@@ -84,7 +84,7 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("copy from a container copies the file to the host from the first container by default", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "nginx:/tmp/default.txt", "./fixtures/cp-test/from-default.txt")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "nginx:/tmp/default.txt", "./fixtures/cp-test/from-default.txt")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
data, err := os.ReadFile("./fixtures/cp-test/from-default.txt")
|
||||
@@ -93,7 +93,7 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("copy from a container with a given index copies the file to host", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3", "nginx:/tmp/indexed.txt", "./fixtures/cp-test/from-indexed.txt")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3", "nginx:/tmp/indexed.txt", "./fixtures/cp-test/from-indexed.txt")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
data, err := os.ReadFile("./fixtures/cp-test/from-indexed.txt")
|
||||
@@ -102,13 +102,13 @@ func TestCopy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("copy to and from a container also work with folder", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "./fixtures/cp-test/cp-folder", "nginx:/tmp")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "./fixtures/cp-test/cp-folder", "nginx:/tmp")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
output := c.RunDockerCmd("exec", projectName+"-nginx-1", "cat", "/tmp/cp-folder/cp-me.txt").Stdout()
|
||||
assert.Assert(t, strings.Contains(output, `hello world from folder`), output)
|
||||
|
||||
res = c.RunDockerCmd("compose", "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "nginx:/tmp/cp-folder", "./fixtures/cp-test/cp-folder2")
|
||||
res = c.RunDockerComposeCmd("-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "nginx:/tmp/cp-folder", "./fixtures/cp-test/cp-folder2")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 0})
|
||||
|
||||
data, err := os.ReadFile("./fixtures/cp-test/cp-folder2/cp-me.txt")
|
||||
|
||||
21
pkg/e2e/e2e_config_plugin.go
Normal file
21
pkg/e2e/e2e_config_plugin.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build !standalone
|
||||
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
const composeStandaloneMode = false
|
||||
21
pkg/e2e/e2e_config_standalone.go
Normal file
21
pkg/e2e/e2e_config_standalone.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build standalone
|
||||
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
const composeStandaloneMode = true
|
||||
19
pkg/e2e/fixtures/build-test/multi-args/Dockerfile
Normal file
19
pkg/e2e/fixtures/build-test/multi-args/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG IMAGE=666
|
||||
ARG TAG=666
|
||||
|
||||
FROM ${IMAGE}:${TAG}
|
||||
RUN echo "SUCCESS"
|
||||
9
pkg/e2e/fixtures/build-test/multi-args/compose.yaml
Normal file
9
pkg/e2e/fixtures/build-test/multi-args/compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
multiargs:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
IMAGE: alpine
|
||||
TAG: latest
|
||||
labels:
|
||||
- RESULT=SUCCESS
|
||||
@@ -1,3 +1,5 @@
|
||||
services:
|
||||
nginx:
|
||||
build: nginx-build
|
||||
build:
|
||||
context: nginx-build
|
||||
dockerfile: Dockerfile
|
||||
|
||||
14
pkg/e2e/fixtures/start-fail/compose.yaml
Normal file
14
pkg/e2e/fixtures/start-fail/compose.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
fail:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
healthcheck:
|
||||
test: "false"
|
||||
interval: 1s
|
||||
retries: 3
|
||||
depends:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
fail:
|
||||
condition: service_healthy
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -39,11 +40,19 @@ import (
|
||||
var (
|
||||
// DockerExecutableName is the OS dependent Docker CLI binary name
|
||||
DockerExecutableName = "docker"
|
||||
|
||||
// DockerComposeExecutableName is the OS dependent Docker CLI binary name
|
||||
DockerComposeExecutableName = "docker-" + compose.PluginName
|
||||
|
||||
// DockerScanExecutableName is the OS dependent Docker CLI binary name
|
||||
DockerScanExecutableName = "docker-scan"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
DockerExecutableName = DockerExecutableName + ".exe"
|
||||
DockerComposeExecutableName = DockerComposeExecutableName + ".exe"
|
||||
DockerScanExecutableName = DockerScanExecutableName + ".exe"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,23 +87,17 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI {
|
||||
})
|
||||
|
||||
_ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755)
|
||||
composePluginFile := "docker-compose"
|
||||
scanPluginFile := "docker-scan"
|
||||
if runtime.GOOS == "windows" {
|
||||
composePluginFile += ".exe"
|
||||
scanPluginFile += ".exe"
|
||||
}
|
||||
composePlugin, err := findExecutable(composePluginFile, []string{"../../bin", "../../../bin"})
|
||||
composePlugin, err := findExecutable(DockerComposeExecutableName, []string{"../../bin", "../../../bin"})
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("WARNING: docker-compose cli-plugin not found")
|
||||
}
|
||||
if err == nil {
|
||||
err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", composePluginFile))
|
||||
err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", DockerComposeExecutableName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// We don't need a functional scan plugin, but a valid plugin binary
|
||||
err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", scanPluginFile))
|
||||
err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", DockerScanExecutableName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -104,9 +107,9 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI {
|
||||
}
|
||||
|
||||
func dirContents(dir string) []string {
|
||||
res := []string{}
|
||||
var res []string
|
||||
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
res = append(res, filepath.Join(dir, path))
|
||||
res = append(res, path)
|
||||
return nil
|
||||
})
|
||||
return res
|
||||
@@ -191,11 +194,29 @@ func (c *E2eCLI) RunCmd(args ...string) *icmd.Result {
|
||||
|
||||
// RunDockerCmd runs a docker command, expects no error and returns a result
|
||||
func (c *E2eCLI) RunDockerCmd(args ...string) *icmd.Result {
|
||||
if len(args) > 0 && args[0] == compose.PluginName {
|
||||
c.test.Fatal("This test called 'RunDockerCmd' for 'compose'. Please prefer 'RunDockerComposeCmd' to be able to test as a plugin and standalone")
|
||||
}
|
||||
res := c.RunDockerOrExitError(args...)
|
||||
res.Assert(c.test, icmd.Success)
|
||||
return res
|
||||
}
|
||||
|
||||
// RunDockerComposeCmd runs a docker compose command, expects no error and returns a result
|
||||
func (c *E2eCLI) RunDockerComposeCmd(args ...string) *icmd.Result {
|
||||
if composeStandaloneMode {
|
||||
composeBinary, err := findExecutable(DockerComposeExecutableName, []string{"../../bin", "../../../bin"})
|
||||
assert.NilError(c.test, err)
|
||||
res := icmd.RunCmd(c.NewCmd(composeBinary, args...))
|
||||
res.Assert(c.test, icmd.Success)
|
||||
return res
|
||||
}
|
||||
args = append([]string{"compose"}, args...)
|
||||
res := icmd.RunCmd(c.NewCmd(DockerExecutableName, args...))
|
||||
res.Assert(c.test, icmd.Success)
|
||||
return res
|
||||
}
|
||||
|
||||
// StdoutContains returns a predicate on command result expecting a string in stdout
|
||||
func StdoutContains(expected string) func(*icmd.Result) bool {
|
||||
return func(res *icmd.Result) bool {
|
||||
@@ -230,7 +251,7 @@ func (c *E2eCLI) WaitForCondition(predicate func() (bool, string), timeout time.
|
||||
poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
|
||||
}
|
||||
|
||||
//Lines split output into lines
|
||||
// Lines split output into lines
|
||||
func Lines(output string) []string {
|
||||
return strings.Split(strings.TrimSpace(output), "\n")
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ func TestIPC(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/ipc-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/ipc-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("check running project", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `shareable`})
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestIPC(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
t.Run("remove ipc mode container", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("rm", "-f", "ipc_mode_container")
|
||||
|
||||
@@ -31,28 +31,28 @@ func TestLocalComposeLogs(t *testing.T) {
|
||||
const projectName = "compose-e2e-logs"
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("logs", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--project-name", projectName, "logs")
|
||||
res := c.RunDockerComposeCmd("--project-name", projectName, "logs")
|
||||
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
|
||||
res.Assert(t, icmd.Expected{Out: `hello`})
|
||||
})
|
||||
|
||||
t.Run("logs ping", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--project-name", projectName, "logs", "ping")
|
||||
res := c.RunDockerComposeCmd("--project-name", projectName, "logs", "ping")
|
||||
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "hello"))
|
||||
})
|
||||
|
||||
t.Run("logs hello", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--project-name", projectName, "logs", "hello", "ping")
|
||||
res := c.RunDockerComposeCmd("--project-name", projectName, "logs", "hello", "ping")
|
||||
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
|
||||
res.Assert(t, icmd.Expected{Out: `hello`})
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/docker/buildx/build"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (s *composeService) windowsBuild(opts map[string]build.Options, mode string) error {
|
||||
// FIXME copy/paste or reuse code from https://github.com/docker/cli/blob/master/cli/command/image/build.go
|
||||
return api.ErrNotImplemented
|
||||
func TestMain(m *testing.M) {
|
||||
exitCode := m.Run()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func TestComposeMetrics(t *testing.T) {
|
||||
res = c.RunDockerOrExitError("compose", "-f", "fixtures/wrong-composefile/compose.yaml", "up", "-d")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 15, Err: "services.simple Additional property wrongField is not allowed"})
|
||||
res = c.RunDockerOrExitError("compose", "up")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 14, Err: "can't find a suitable configuration file in this directory or any parent: not found"})
|
||||
res.Assert(t, icmd.Expected{ExitCode: 14, Err: "no configuration file provided: not found"})
|
||||
res = c.RunDockerOrExitError("compose", "up", "-f", "fixtures/wrong-composefile/compose.yaml")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 16, Err: "unknown shorthand flag: 'f' in -f"})
|
||||
res = c.RunDockerOrExitError("compose", "up", "--file", "fixtures/wrong-composefile/compose.yaml")
|
||||
|
||||
@@ -37,11 +37,11 @@ func TestNetworks(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("check running project", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
|
||||
res := c.RunDockerComposeCmd("-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `web`})
|
||||
|
||||
endpoint := "http://localhost:80"
|
||||
@@ -54,12 +54,12 @@ func TestNetworks(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("port", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "--project-name", projectName, "port", "words", "8080")
|
||||
res := c.RunDockerComposeCmd("--project-name", projectName, "port", "words", "8080")
|
||||
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
|
||||
t.Run("check networks after down", func(t *testing.T) {
|
||||
@@ -75,21 +75,21 @@ func TestNetworkAliassesAndLinks(t *testing.T) {
|
||||
const projectName = "network_alias_e2e"
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("curl alias", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "http://alias-of-container2/")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "http://alias-of-container2/")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("curl links", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "http://container/")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "http://container/")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestIPAMConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("up", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/ipam/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/ipam/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("ensure service get fixed IP assigned", func(t *testing.T) {
|
||||
@@ -112,6 +112,22 @@ func TestIPAMConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkModes(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
const projectName = "network_mode_service_run"
|
||||
|
||||
t.Run("run with service mode dependency", func(t *testing.T) {
|
||||
res := c.RunDockerOrExitError("compose", "-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "run", "-T", "mydb", "echo", "success")
|
||||
res.Assert(t, icmd.Expected{Out: "success"})
|
||||
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,18 +36,18 @@ func TestDisplayScanMessageAfterBuild(t *testing.T) {
|
||||
c.RunDockerOrExitError("scan", "--help")
|
||||
|
||||
t.Run("display on compose build", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-compose-build", "build")
|
||||
res := c.RunDockerComposeCmd("-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-compose-build", "build")
|
||||
defer c.RunDockerOrExitError("rmi", "-f", "scan-msg-test-compose-build_nginx")
|
||||
res.Assert(t, icmd.Expected{Err: utils.ScanSuggestMsg})
|
||||
})
|
||||
|
||||
t.Run("do not display on compose build with quiet flag", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-quiet", "build", "--quiet")
|
||||
res := c.RunDockerComposeCmd("-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-quiet", "build", "--quiet")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined())
|
||||
res = c.RunDockerCmd("rmi", "-f", "scan-msg-test-quiet_nginx")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "No such image"))
|
||||
|
||||
res = c.RunDockerCmd("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-q", "build", "-q")
|
||||
res = c.RunDockerComposeCmd("-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test-q", "build", "-q")
|
||||
defer c.RunDockerOrExitError("rmi", "-f", "scan-msg-test-q_nginx")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined())
|
||||
})
|
||||
@@ -55,13 +55,13 @@ func TestDisplayScanMessageAfterBuild(t *testing.T) {
|
||||
_ = c.RunDockerOrExitError("rmi", "scan-msg-test_nginx")
|
||||
|
||||
t.Run("display on compose up if image is built", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "up", "-d")
|
||||
res := c.RunDockerComposeCmd("-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "up", "-d")
|
||||
defer c.RunDockerOrExitError("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "down")
|
||||
res.Assert(t, icmd.Expected{Err: utils.ScanSuggestMsg})
|
||||
})
|
||||
|
||||
t.Run("do not display on compose up if no image built", func(t *testing.T) { // re-run the same Compose aproject
|
||||
res := c.RunDockerCmd("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "up", "-d")
|
||||
res := c.RunDockerComposeCmd("-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "up", "-d")
|
||||
defer c.RunDockerOrExitError("compose", "-f", "fixtures/simple-build-test/compose.yaml", "-p", "scan-msg-test", "down", "--rmi", "all")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined())
|
||||
})
|
||||
|
||||
33
pkg/e2e/start_fail_test.go
Normal file
33
pkg/e2e/start_fail_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
func TestStartFail(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
const projectName = "e2e-start-fail"
|
||||
|
||||
res := c.RunDockerOrExitError("compose", "-f", "fixtures/start-fail/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container for service "fail" is unhealthy`})
|
||||
|
||||
c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
}
|
||||
@@ -42,56 +42,56 @@ func TestStartStop(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Up a project", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-simple-1 Started"), res.Combined())
|
||||
|
||||
res = c.RunDockerCmd("compose", "ls", "--all")
|
||||
res = c.RunDockerComposeCmd("ls", "--all")
|
||||
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
|
||||
|
||||
res = c.RunDockerCmd("compose", "--project-name", projectName, "ps")
|
||||
res = c.RunDockerComposeCmd("--project-name", projectName, "ps")
|
||||
testify.Regexp(t, getServiceRegx("simple", "running"), res.Stdout())
|
||||
testify.Regexp(t, getServiceRegx("another", "running"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("stop project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "stop")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "stop")
|
||||
|
||||
res := c.RunDockerCmd("compose", "ls")
|
||||
res := c.RunDockerComposeCmd("ls")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop"), res.Combined())
|
||||
|
||||
res = c.RunDockerCmd("compose", "ls", "--all")
|
||||
res = c.RunDockerComposeCmd("ls", "--all")
|
||||
testify.Regexp(t, getProjectRegx("exited"), res.Stdout())
|
||||
|
||||
res = c.RunDockerCmd("compose", "--project-name", projectName, "ps")
|
||||
res = c.RunDockerComposeCmd("--project-name", projectName, "ps")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-words-1"), res.Combined())
|
||||
|
||||
res = c.RunDockerCmd("compose", "--project-name", projectName, "ps", "--all")
|
||||
res = c.RunDockerComposeCmd("--project-name", projectName, "ps", "--all")
|
||||
testify.Regexp(t, getServiceRegx("simple", "exited"), res.Stdout())
|
||||
testify.Regexp(t, getServiceRegx("another", "exited"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("start project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "start")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "start")
|
||||
|
||||
res := c.RunDockerCmd("compose", "ls")
|
||||
res := c.RunDockerComposeCmd("ls")
|
||||
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("pause project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "pause")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "pause")
|
||||
|
||||
res := c.RunDockerCmd("compose", "ls", "--all")
|
||||
res := c.RunDockerComposeCmd("ls", "--all")
|
||||
testify.Regexp(t, getProjectRegx("paused"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("unpause project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "unpause")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "unpause")
|
||||
|
||||
res := c.RunDockerCmd("compose", "ls")
|
||||
res := c.RunDockerComposeCmd("ls")
|
||||
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
_ = c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestLocalComposeVolume(t *testing.T) {
|
||||
c.RunDockerOrExitError("rmi", "compose-e2e-volume_nginx")
|
||||
c.RunDockerOrExitError("volume", "rm", projectName+"_staticVol")
|
||||
c.RunDockerOrExitError("volume", "rm", "myvolume")
|
||||
c.RunDockerCmd("compose", "--project-directory", "fixtures/volume-test", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd("--project-directory", "fixtures/volume-test", "--project-name", projectName, "up", "-d")
|
||||
})
|
||||
|
||||
t.Run("access bind mount data", func(t *testing.T) {
|
||||
@@ -82,9 +82,9 @@ func TestLocalComposeVolume(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("cleanup volume project", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "--project-name", projectName, "down", "--volumes")
|
||||
res := c.RunDockerCmd("volume", "ls")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), projectName+"_staticVol"))
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "myvolume"))
|
||||
c.RunDockerComposeCmd("--project-name", projectName, "down", "--volumes")
|
||||
ls := c.RunDockerCmd("volume", "ls").Stdout()
|
||||
assert.Assert(t, !strings.Contains(ls, projectName+"_staticVol"))
|
||||
assert.Assert(t, !strings.Contains(ls, "myvolume"))
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,21 @@ func StartedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Started")
|
||||
}
|
||||
|
||||
// Waiting creates a new waiting event
|
||||
func Waiting(ID string) Event {
|
||||
return NewEvent(ID, Working, "Waiting")
|
||||
}
|
||||
|
||||
// Healthy creates a new healthy event
|
||||
func Healthy(ID string) Event {
|
||||
return NewEvent(ID, Done, "Healthy")
|
||||
}
|
||||
|
||||
// Exited creates a new exited event
|
||||
func Exited(ID string) Event {
|
||||
return NewEvent(ID, Done, "Exited")
|
||||
}
|
||||
|
||||
// RestartingEvent creates a new Restarting in progress Event
|
||||
func RestartingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Restarting")
|
||||
|
||||
@@ -27,7 +27,10 @@ func (p *noopWriter) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *noopWriter) Event(e Event) {
|
||||
func (p *noopWriter) Event(Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Events([]Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
|
||||
|
||||
@@ -40,6 +40,12 @@ func (p *plainWriter) Event(e Event) {
|
||||
fmt.Fprintln(p.out, e.ID, e.Text, e.StatusText)
|
||||
}
|
||||
|
||||
func (p *plainWriter) Events(events []Event) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *plainWriter) TailMsgf(m string, args ...interface{}) {
|
||||
fmt.Fprintln(p.out, append([]interface{}{m}, args...)...)
|
||||
}
|
||||
|
||||
@@ -38,12 +38,13 @@ type ttyWriter struct {
|
||||
repeated bool
|
||||
numLines int
|
||||
done chan bool
|
||||
mtx *sync.RWMutex
|
||||
mtx *sync.Mutex
|
||||
tailEvents []string
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -94,6 +95,12 @@ func (w *ttyWriter) Event(e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Events(events []Event) {
|
||||
for _, e := range events {
|
||||
w.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) TailMsgf(msg string, args ...interface{}) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
@@ -188,7 +195,7 @@ func lineText(event Event, pad string, terminalWidth, statusPadding int, color b
|
||||
padding = 0
|
||||
}
|
||||
// calculate the max length for the status text, on errors it
|
||||
// is 2-3 lines long and breaks the line formating
|
||||
// is 2-3 lines long and breaks the line formatting
|
||||
maxStatusLen := terminalWidth - textLen - statusPadding - 15
|
||||
status := event.StatusText
|
||||
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestLineTextSingleEvent(t *testing.T) {
|
||||
func TestErrorEvent(t *testing.T) {
|
||||
w := &ttyWriter{
|
||||
events: map[string]Event{},
|
||||
mtx: &sync.RWMutex{},
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
e := Event{
|
||||
ID: "id",
|
||||
|
||||
@@ -31,6 +31,7 @@ type Writer interface {
|
||||
Start(context.Context) error
|
||||
Stop()
|
||||
Event(Event)
|
||||
Events([]Event)
|
||||
TailMsgf(string, ...interface{})
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ func NewWriter(out console.File) (Writer, error) {
|
||||
events: map[string]Event{},
|
||||
repeated: false,
|
||||
done: make(chan bool),
|
||||
mtx: &sync.RWMutex{},
|
||||
mtx: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringContains check if an array contains a specific value
|
||||
func StringContains(array []string, needle string) bool {
|
||||
for _, val := range array {
|
||||
@@ -25,3 +30,9 @@ func StringContains(array []string, needle string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringToBool converts a string to a boolean ignoring errors
|
||||
func StringToBool(s string) bool {
|
||||
b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s)))
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user