mirror of
https://github.com/docker/compose.git
synced 2026-02-10 02:29:25 +08:00
Compare commits
328 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7abaa06617 | ||
|
|
3b0e8f538e | ||
|
|
af376603c3 | ||
|
|
7f8814f4c5 | ||
|
|
af0029afe1 | ||
|
|
b76feb66e1 | ||
|
|
9dc7f1e70c | ||
|
|
03205124fe | ||
|
|
8b769bad6b | ||
|
|
671507a8b3 | ||
|
|
56ab28aef3 | ||
|
|
e7d870a106 | ||
|
|
d5bb3387ca | ||
|
|
d91fc63813 | ||
|
|
c51b1fea29 | ||
|
|
fa7549a851 | ||
|
|
a061c17736 | ||
|
|
c5e7d9158c | ||
|
|
3783b8ada3 | ||
|
|
c428a77111 | ||
|
|
04b4a832dc | ||
|
|
27faa3b84e | ||
|
|
bcc0401e0e | ||
|
|
093205121c | ||
|
|
b92b87dd9c | ||
|
|
06e1287483 | ||
|
|
d7bdb34ff5 | ||
|
|
79d7a8acd6 | ||
|
|
abd99be4fd | ||
|
|
2672d34217 | ||
|
|
27bf40357a | ||
|
|
c8d687599a | ||
|
|
2f108ffaa8 | ||
|
|
0a07df0e5b | ||
|
|
02b606ef8e | ||
|
|
9856802945 | ||
|
|
63ae7eb0fa | ||
|
|
f17d0dfc61 | ||
|
|
ef14cfcfea | ||
|
|
b760afaf9f | ||
|
|
a2a5c86f53 | ||
|
|
98e82127b3 | ||
|
|
03e19e4a84 | ||
|
|
b2c17ff118 | ||
|
|
ec88588cd8 | ||
|
|
7d5913403a | ||
|
|
d95aa57f01 | ||
|
|
ee4c01b66b | ||
|
|
d7a65f53f8 | ||
|
|
4520bcbaf6 | ||
|
|
327be1fcd5 | ||
|
|
59f04b85af | ||
|
|
b4574c8bd6 | ||
|
|
29d6c918c4 | ||
|
|
58403169f3 | ||
|
|
6aee7f8370 | ||
|
|
c89b8a2d6b | ||
|
|
aec9f54176 | ||
|
|
232197d364 | ||
|
|
81ba889bee | ||
|
|
8e5b25c0f1 | ||
|
|
d4c1987638 | ||
|
|
1297f97aef | ||
|
|
55cded1806 | ||
|
|
6c043929a0 | ||
|
|
2750330566 | ||
|
|
e22426443e | ||
|
|
6599f8ad84 | ||
|
|
3853ad3911 | ||
|
|
02008a0097 | ||
|
|
4f419e5098 | ||
|
|
b62cbed87c | ||
|
|
aa9a71f37a | ||
|
|
ac211e6e51 | ||
|
|
778a627b8e | ||
|
|
359d2f076e | ||
|
|
c9e0d83e14 | ||
|
|
3e206fdcc6 | ||
|
|
d12947e9f8 | ||
|
|
0878c59a74 | ||
|
|
c0345e4f45 | ||
|
|
9fada6cc23 | ||
|
|
85ea24b62c | ||
|
|
000a4a4b9f | ||
|
|
08de90c267 | ||
|
|
cfcee45a89 | ||
|
|
13d70b1c11 | ||
|
|
72f4d655ef | ||
|
|
dc66e6bad1 | ||
|
|
8d9d5259e0 | ||
|
|
b32297dccd | ||
|
|
af8cac5768 | ||
|
|
8477a85ce6 | ||
|
|
6ee7146354 | ||
|
|
f28503426c | ||
|
|
e0977c2df1 | ||
|
|
2d569916fe | ||
|
|
3975f02153 | ||
|
|
fa832d72d7 | ||
|
|
822f5a702b | ||
|
|
68bb7a71ba | ||
|
|
6f365395e5 | ||
|
|
3052934624 | ||
|
|
428abab16a | ||
|
|
755618e707 | ||
|
|
c47b8c32e3 | ||
|
|
89d3944837 | ||
|
|
f2b14fe1aa | ||
|
|
bd2257b6d1 | ||
|
|
d7e5f20eb6 | ||
|
|
2b4543935c | ||
|
|
f0dce1b977 | ||
|
|
6e55832b1c | ||
|
|
45def51117 | ||
|
|
aff5c115d6 | ||
|
|
5ef495c898 | ||
|
|
9de7e2a388 | ||
|
|
dc90c4e44d | ||
|
|
91e1753d80 | ||
|
|
9db27a65c6 | ||
|
|
efd7424da7 | ||
|
|
02109c8d33 | ||
|
|
c37ede62db | ||
|
|
7eb5adeef6 | ||
|
|
0793ad7c68 | ||
|
|
8137b2bce8 | ||
|
|
4e3372b473 | ||
|
|
fef26fb372 | ||
|
|
a32e13a2b0 | ||
|
|
67e39a41f2 | ||
|
|
dc1283289d | ||
|
|
0c596ed3cf | ||
|
|
13870006fb | ||
|
|
af579ebd4b | ||
|
|
fc2a7d13fa | ||
|
|
d70bb8cf5e | ||
|
|
bff3d35305 | ||
|
|
b80bb0586e | ||
|
|
d74274bc04 | ||
|
|
10f15cacdd | ||
|
|
3658a063bb | ||
|
|
74a4ccdd85 | ||
|
|
6719f47bd4 | ||
|
|
3eb2934eb7 | ||
|
|
c416ea7036 | ||
|
|
0d396bbacb | ||
|
|
fc74c78963 | ||
|
|
658bff335f | ||
|
|
80030e1390 | ||
|
|
6a35be5112 | ||
|
|
0c854a6ab7 | ||
|
|
557e0b6ec7 | ||
|
|
a8933c91e7 | ||
|
|
7e3993bcac | ||
|
|
fd4f2f99cf | ||
|
|
ae25d27e5a | ||
|
|
394466683a | ||
|
|
e5c8b68642 | ||
|
|
bf50c99193 | ||
|
|
8274be8d08 | ||
|
|
86e91e010d | ||
|
|
e1678c5c43 | ||
|
|
5924387e89 | ||
|
|
7f668bd7fe | ||
|
|
3ce52883cb | ||
|
|
ac3b8fd8a5 | ||
|
|
8619f5d72a | ||
|
|
e59150baa8 | ||
|
|
6a90742ef2 | ||
|
|
6007d4c7e7 | ||
|
|
69bcb962bf | ||
|
|
9b4fcce034 | ||
|
|
da5c57c29d | ||
|
|
e25265dd55 | ||
|
|
e19e1278b5 | ||
|
|
585c4db4f9 | ||
|
|
be8c7e6c60 | ||
|
|
27f59d7f42 | ||
|
|
2681ed17a7 | ||
|
|
ee75be342b | ||
|
|
157617480a | ||
|
|
88aae9c46e | ||
|
|
7755302348 | ||
|
|
147923c44c | ||
|
|
289faae5fa | ||
|
|
e7aa484b78 | ||
|
|
ae3309afab | ||
|
|
0b5fb36eb5 | ||
|
|
63920c4cc0 | ||
|
|
a03f2562df | ||
|
|
a07f2b8ded | ||
|
|
f45a3ebcfd | ||
|
|
7fec70b6c7 | ||
|
|
ce463d50b2 | ||
|
|
fa7e85ed83 | ||
|
|
d9423f6872 | ||
|
|
5add90240d | ||
|
|
07602f2070 | ||
|
|
cf7e31f731 | ||
|
|
fa08127456 | ||
|
|
4ee52ad168 | ||
|
|
4a4776ec57 | ||
|
|
713de5bb9e | ||
|
|
9ded1684cd | ||
|
|
8bc8593fd0 | ||
|
|
8978c1027d | ||
|
|
032e0309ee | ||
|
|
38ba35e165 | ||
|
|
56e0ba8080 | ||
|
|
9752fa5500 | ||
|
|
4761fd88b0 | ||
|
|
02c8e63545 | ||
|
|
ab7a6e9322 | ||
|
|
2ca7b96e33 | ||
|
|
a32dc3da72 | ||
|
|
db260938c1 | ||
|
|
5aea94794c | ||
|
|
d07c437ce8 | ||
|
|
da72230c39 | ||
|
|
a429c09dfa | ||
|
|
df3c27c864 | ||
|
|
956891af54 | ||
|
|
a473341058 | ||
|
|
385b3f5c96 | ||
|
|
2d482e61ce | ||
|
|
c75418ee07 | ||
|
|
0cdc5c9bff | ||
|
|
b768232c0e | ||
|
|
09689400e5 | ||
|
|
cb3691154b | ||
|
|
b387ba4a05 | ||
|
|
7cd569922e | ||
|
|
eec2bb7ea6 | ||
|
|
2c15aef2ed | ||
|
|
290366205b | ||
|
|
a91ca95a71 | ||
|
|
beb81a73f9 | ||
|
|
f217207876 | ||
|
|
02ffe2ac6c | ||
|
|
f48131fb66 | ||
|
|
4dd369bdcb | ||
|
|
ad73766bf2 | ||
|
|
3c1f5a1815 | ||
|
|
42d1e4c333 | ||
|
|
6ca8663bda | ||
|
|
b33ecf65e8 | ||
|
|
04b8ac5fe4 | ||
|
|
d09948da41 | ||
|
|
f1efbb8322 | ||
|
|
1d52012b82 | ||
|
|
1d69f4a68c | ||
|
|
6078b4d99d | ||
|
|
73e593e69a | ||
|
|
51499f645b | ||
|
|
5165b0f814 | ||
|
|
93dd1a4558 | ||
|
|
ba3f5664c0 | ||
|
|
c420bc44c4 | ||
|
|
60681a824c | ||
|
|
19ad737ee7 | ||
|
|
d3a260e533 | ||
|
|
e75329dce2 | ||
|
|
1dc0be2c30 | ||
|
|
3bac9ffd08 | ||
|
|
f266715dd0 | ||
|
|
c2cb0aef6b | ||
|
|
fbc62d111e | ||
|
|
0d40064ce8 | ||
|
|
91a6eafa1d | ||
|
|
f36ee00f71 | ||
|
|
29ede3ba7d | ||
|
|
bf6d7bf47e | ||
|
|
fc66da06db | ||
|
|
909211dd61 | ||
|
|
0dc9852c67 | ||
|
|
a478702236 | ||
|
|
2c12ad19db | ||
|
|
038ea8441a | ||
|
|
9e98e6101e | ||
|
|
52f04229c0 | ||
|
|
28895d0322 | ||
|
|
a926f7d717 | ||
|
|
fe046915eb | ||
|
|
adbd61e5d6 | ||
|
|
e37ac04329 | ||
|
|
cab2c2a44e | ||
|
|
1946de598d | ||
|
|
8e29a138aa | ||
|
|
3c8da0afee | ||
|
|
1b12c867c5 | ||
|
|
1a4fc55fd7 | ||
|
|
efc939dcee | ||
|
|
d6e9f79ba6 | ||
|
|
b4c44a431f | ||
|
|
fb5a8644c3 | ||
|
|
95660c5e5a | ||
|
|
f6ddd6ae88 | ||
|
|
4ae7066955 | ||
|
|
fd954f266c | ||
|
|
d62e21025c | ||
|
|
6a2d16bd10 | ||
|
|
4d47da6dc2 | ||
|
|
8f91793fb5 | ||
|
|
1d2223fb23 | ||
|
|
d4f6000712 | ||
|
|
c50d16cd78 | ||
|
|
3875e13fad | ||
|
|
c89f30170d | ||
|
|
41a9b91887 | ||
|
|
5fc2b2a71c | ||
|
|
b1cd40c316 | ||
|
|
362ab0733f | ||
|
|
f35d2cfb3b | ||
|
|
17ba6c7188 | ||
|
|
1c37f1abb6 | ||
|
|
485b6200ee | ||
|
|
8c17a35609 | ||
|
|
6b9667401a | ||
|
|
9a1e589ce8 | ||
|
|
5e147e852e | ||
|
|
29308cb97e | ||
|
|
0b0242d0ac | ||
|
|
5a704004d3 | ||
|
|
cb95910018 | ||
|
|
f42226e352 | ||
|
|
0cc3c7a550 | ||
|
|
f7ee9c8a0c | ||
|
|
35efa97b7d |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -12,6 +12,12 @@ body:
|
||||
Include both the current behavior (what you are seeing) as well as what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
[Docker Swarm](https://www.mirantis.com/software/swarm/) uses a distinct compose file parser and
|
||||
as such doesn't support some of the recent features of Docker Compose. Please contact Mirantis
|
||||
if you need assistance with compose file support in Docker Swarm.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
|
||||
185
.github/workflows/ci.yml
vendored
185
.github/workflows/ci.yml
vendored
@@ -22,24 +22,6 @@ permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
run: |
|
||||
echo matrix=$(docker buildx bake binary-cross --print | jq -cr '.target."binary-cross".platforms') >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Show matrix
|
||||
run: |
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -63,63 +45,88 @@ jobs:
|
||||
make ${{ matrix.target }}
|
||||
|
||||
binary:
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) with GitHub OIDC Token
|
||||
with:
|
||||
runner: amd64
|
||||
artifact-name: compose
|
||||
artifact-upload: true
|
||||
cache: true
|
||||
cache-scope: binary
|
||||
target: release
|
||||
output: local
|
||||
sbom: true
|
||||
sign: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
binary-finalize:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
||||
- binary
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Prepare
|
||||
run: |
|
||||
platform=${MATRIX_PLATFORM}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
env:
|
||||
MATRIX_PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
source: .
|
||||
targets: release
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
set: |
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }}
|
||||
*.cache-to=type=gha,scope=binary-${{ env.PLATFORM_PAIR }},mode=max
|
||||
path: /tmp/compose-output
|
||||
name: ${{ needs.binary.outputs.artifact-name }}
|
||||
-
|
||||
name: Rename provenance and sbom
|
||||
run: |
|
||||
for pdir in /tmp/compose-output/*/; do
|
||||
(
|
||||
cd "$pdir"
|
||||
binname=$(find . -name 'docker-compose-*')
|
||||
filename=$(basename "${binname%.exe}")
|
||||
mv "provenance.json" "${filename}.provenance.json"
|
||||
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
|
||||
find . -name 'sbom*.json' -exec rm {} \;
|
||||
if [ -f "provenance.sigstore.json" ]; then
|
||||
mv "provenance.sigstore.json" "${filename}.sigstore.json"
|
||||
fi
|
||||
)
|
||||
done
|
||||
mkdir -p "./bin/release"
|
||||
mv /tmp/compose-output/**/* "./bin/release/"
|
||||
-
|
||||
name: Create checksum file
|
||||
working-directory: ./bin/release
|
||||
run: |
|
||||
binname=$(find . -name 'docker-compose-*')
|
||||
filename=$(basename "$binname" | sed -E 's/\.exe$//')
|
||||
mv "provenance.json" "${filename}.provenance.json"
|
||||
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
|
||||
find . -name 'sbom*.json' -exec rm {} \;
|
||||
-
|
||||
name: List artifacts
|
||||
run: |
|
||||
tree -nh ./bin/release
|
||||
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
|
||||
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
|
||||
mv $RUNNER_TEMP/checksums.txt .
|
||||
cat checksums.txt | while read sum file; do
|
||||
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json && "${file#\*}" != *.sigstore.json ]]; then
|
||||
echo "$sum $file" > ${file#\*}.sha256
|
||||
fi
|
||||
done
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: compose-${{ env.PLATFORM_PAIR }}
|
||||
path: ./bin/release
|
||||
name: release
|
||||
path: ./bin/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
bin-image-test:
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
with:
|
||||
runner: amd64
|
||||
target: image-cross
|
||||
cache: true
|
||||
cache-scope: bin-image-test
|
||||
output: image
|
||||
push: false
|
||||
sbom: true
|
||||
set-meta-labels: true
|
||||
meta-images: |
|
||||
compose-bin
|
||||
meta-tags: |
|
||||
type=ref,event=pr
|
||||
meta-bake-target: meta-helper
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -147,18 +154,29 @@ jobs:
|
||||
with:
|
||||
paths: bin/coverage/unit/report.xml
|
||||
if: always()
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
name: e2e (${{ matrix.mode }}, ${{ matrix.channel }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mode:
|
||||
- plugin
|
||||
- standalone
|
||||
engine:
|
||||
- 26
|
||||
- 27
|
||||
- 28
|
||||
include:
|
||||
# current stable
|
||||
- mode: plugin
|
||||
engine: 29
|
||||
channel: stable
|
||||
- mode: standalone
|
||||
engine: 29
|
||||
channel: stable
|
||||
|
||||
# old stable (latest major - 1)
|
||||
- mode: plugin
|
||||
engine: 28
|
||||
channel: oldstable
|
||||
- mode: standalone
|
||||
engine: 28
|
||||
channel: oldstable
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -189,9 +207,9 @@ jobs:
|
||||
docker model version
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: '.go-version'
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
@@ -244,6 +262,7 @@ jobs:
|
||||
with:
|
||||
paths: /tmp/report/report.xml
|
||||
if: always()
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -254,9 +273,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: '.go-version'
|
||||
check-latest: true
|
||||
- name: Download unit test coverage
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -280,40 +299,26 @@ jobs:
|
||||
path: ./coverage.txt
|
||||
if-no-files-found: error
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: write # to create a release (ncipollo/release-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- binary
|
||||
- binary-finalize
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: compose-*
|
||||
path: ./bin/release
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Create checksums
|
||||
working-directory: ./bin/release
|
||||
run: |
|
||||
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
|
||||
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
|
||||
mv $RUNNER_TEMP/checksums.txt .
|
||||
cat checksums.txt | while read sum file; do
|
||||
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json ]]; then
|
||||
echo "$sum $file" > ${file#\*}.sha256
|
||||
fi
|
||||
done
|
||||
name: release
|
||||
-
|
||||
name: List artifacts
|
||||
run: |
|
||||
|
||||
90
.github/workflows/merge.yml
vendored
90
.github/workflows/merge.yml
vendored
@@ -33,9 +33,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version-file: '.go-version'
|
||||
cache: true
|
||||
check-latest: true
|
||||
|
||||
@@ -74,63 +74,41 @@ jobs:
|
||||
run: |
|
||||
make e2e-compose-standalone
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-22.04
|
||||
bin-image-prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
|
||||
repo-slug: ${{ env.REPO_SLUG }}
|
||||
steps:
|
||||
-
|
||||
name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
# FIXME: can't use env object in reusable workflow inputs: https://github.com/orgs/community/discussions/26671
|
||||
- run: echo "Exposing env vars for reusable workflow"
|
||||
|
||||
bin-image:
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
needs:
|
||||
- bin-image-prepare
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) with GitHub OIDC Token
|
||||
with:
|
||||
runner: amd64
|
||||
target: image-cross
|
||||
cache: true
|
||||
cache-scope: bin-image
|
||||
output: image
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
sbom: true
|
||||
set-meta-labels: true
|
||||
meta-images: |
|
||||
${{ needs.bin-image-prepare.outputs.repo-slug }}
|
||||
meta-tags: |
|
||||
type=ref,event=tag
|
||||
type=edge
|
||||
meta-bake-target: meta-helper
|
||||
secrets:
|
||||
registry-auths: |
|
||||
- registry: docker.io
|
||||
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
|
||||
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REPO_SLUG }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=edge
|
||||
bake-target: meta-helper
|
||||
-
|
||||
name: Build and push image
|
||||
uses: docker/bake-action@v6
|
||||
id: bake
|
||||
with:
|
||||
source: .
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
${{ steps.meta.outputs.bake-file }}
|
||||
targets: image-cross
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
sbom: true
|
||||
provenance: mode=max
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=bin-image
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
|
||||
desktop-edge-test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -158,6 +136,6 @@ jobs:
|
||||
workflow_id: 'compose-edge-integration.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"image-tag": "${{ needs.bin-image.outputs.digest }}"
|
||||
"image-tag": "${{ env.REPO_SLUG }}:edge"
|
||||
}
|
||||
})
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ bin/
|
||||
coverage.out
|
||||
covdatafiles/
|
||||
.DS_Store
|
||||
pkg/e2e/*.tar
|
||||
|
||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
||||
1.25.7
|
||||
@@ -8,6 +8,7 @@ linters:
|
||||
- depguard
|
||||
- errcheck
|
||||
- errorlint
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gomodguard
|
||||
@@ -30,12 +31,23 @@ linters:
|
||||
deny:
|
||||
- pkg: io/ioutil
|
||||
desc: io/ioutil package has been deprecated
|
||||
- pkg: github.com/docker/docker/errdefs
|
||||
desc: use github.com/containerd/errdefs instead.
|
||||
- pkg: golang.org/x/exp/maps
|
||||
desc: use stdlib maps package
|
||||
- pkg: golang.org/x/exp/slices
|
||||
desc: use stdlib slices package
|
||||
- pkg: gopkg.in/yaml.v2
|
||||
desc: compose-go uses yaml.v3
|
||||
forbidigo:
|
||||
analyze-types: true
|
||||
forbid:
|
||||
- pattern: 'context\.Background'
|
||||
pkg: '^context$'
|
||||
msg: "in tests, use t.Context() instead of context.Background()"
|
||||
- pattern: 'context\.TODO'
|
||||
pkg: '^context$'
|
||||
msg: "in tests, use t.Context() instead of context.TODO()"
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- paramTypeCombine
|
||||
@@ -72,16 +84,27 @@ linters:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
rules:
|
||||
- path-except: '_test\.go'
|
||||
linters:
|
||||
- forbidigo
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- localmodule
|
||||
custom-order: true # make the section order the same as the order of "sections".
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* Windows:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
* macOS:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
* Linux:
|
||||
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
|
||||
### Building the CLI
|
||||
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -15,9 +15,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.23.10
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOLANGCI_LINT_VERSION=v2.0.2
|
||||
ARG GO_VERSION=1.25.7
|
||||
ARG XX_VERSION=1.9.0
|
||||
ARG GOLANGCI_LINT_VERSION=v2.8.0
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e"
|
||||
@@ -28,12 +28,12 @@ ARG LICENSE_FILES=".*\(Dockerfile\|Makefile\|\.go\|\.hcl\|\.sh\)"
|
||||
FROM --platform=${BUILDPLATFORM} tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
# osxcross contains the MacOSX cross toolchain for xx
|
||||
FROM crazymax/osxcross:11.3-alpine AS osxcross
|
||||
FROM crazymax/osxcross:15.5-alpine AS osxcross
|
||||
|
||||
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
|
||||
FROM ghcr.io/google/addlicense:${ADDLICENSE_VERSION} AS addlicense
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS base
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine3.22 AS base
|
||||
COPY --from=xx / /
|
||||
RUN apk add --no-cache \
|
||||
clang \
|
||||
@@ -83,7 +83,7 @@ RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
|
||||
xx-go --wrap && \
|
||||
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
|
||||
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; export BUILD_TAGS=fsnotify,$BUILD_TAGS; fi && \
|
||||
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
|
||||
xx-verify --static /out/docker-compose
|
||||
|
||||
|
||||
124
MAINTAINERS
124
MAINTAINERS
@@ -1,124 +0,0 @@
|
||||
# Docker maintainers file
|
||||
#
|
||||
# This file describes who runs the docker/compose project and how.
|
||||
# This is a living document - if you see something out of date or missing, speak up!
|
||||
#
|
||||
# It is structured to be consumable by both humans and programs.
|
||||
# To extract its contents programmatically, use any TOML-compliant
|
||||
# parser.
|
||||
#
|
||||
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
||||
#
|
||||
[Org]
|
||||
|
||||
[Org."Core maintainers"]
|
||||
|
||||
# The Core maintainers are the ghostbusters of the project: when there's a problem others
|
||||
# can't solve, they show up and fix it with bizarre devices and weaponry.
|
||||
# They have final say on technical implementation and coding style.
|
||||
# They are ultimately responsible for quality in all its forms: usability polish,
|
||||
# bugfixes, performance, stability, etc. When ownership can cleanly be passed to
|
||||
# a subsystem, they are responsible for doing so and holding the
|
||||
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
|
||||
|
||||
people = [
|
||||
"glours",
|
||||
"jhrotko",
|
||||
"milas",
|
||||
"ndeloof",
|
||||
"nicksieger",
|
||||
"StefanScherer",
|
||||
"ulyssessouza"
|
||||
]
|
||||
|
||||
[Org."Regular maintainers"]
|
||||
# The Regular maintainers are people who aren't Core maintainers but are around
|
||||
# to help reviewing and fixing bugs, just on a less regular basis than previously.
|
||||
# Most of them were previously Core maintainers of Compose.
|
||||
people = [
|
||||
"aiordache",
|
||||
"chris-crone",
|
||||
"gtardif",
|
||||
"laurazard",
|
||||
"maxcleme",
|
||||
"rumpl",
|
||||
"thaJeztah"
|
||||
]
|
||||
|
||||
[people]
|
||||
|
||||
# A reference list of all people associated with the project.
|
||||
# All other sections should refer to people by their canonical key
|
||||
# in the people section.
|
||||
|
||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||
|
||||
[people.aiordache]
|
||||
Name = "Anca Iordache"
|
||||
Email = "anca.iordache@docker.com"
|
||||
GitHub = "aiordache "
|
||||
|
||||
[people.chris-crone]
|
||||
Name = "Christopher Crone"
|
||||
Email = "christopher.crone@docker.com"
|
||||
GitHub = "chris-crone"
|
||||
|
||||
[people.glours]
|
||||
Name = "Guillaume Lours"
|
||||
Email = "guillaume.lours@docker.com"
|
||||
GitHub = "glours"
|
||||
|
||||
[people.gtardif]
|
||||
Name = "Guillaume Tardif"
|
||||
Email = "guillaume.tardif@docker.com"
|
||||
GitHub = "gtardif"
|
||||
|
||||
[people.jhrotko]
|
||||
Name = "Joana Hrotko"
|
||||
Email = "joana.hrotko@docker.com"
|
||||
Github = "jhrotko"
|
||||
|
||||
[people.laurazard]
|
||||
Name = "Laura Brehm"
|
||||
Email = "laura.brehm@docker.com"
|
||||
GitHub = "laurazard"
|
||||
|
||||
[people.maxcleme]
|
||||
Name = "Maxime Clement"
|
||||
Email = "maxime.clement@docker.com"
|
||||
GitHub = "maxcleme"
|
||||
|
||||
[people.milas]
|
||||
Name = "Milas Bowman"
|
||||
Email = "milas.bowman@docker.com"
|
||||
GitHub = "milas"
|
||||
|
||||
[people.nicksieger]
|
||||
Name = "Nick Sieger"
|
||||
Email = "nick.sieger@docker.com"
|
||||
GitHub = "nicksieger"
|
||||
|
||||
[people.ndeloof]
|
||||
Name = "Nicolas Deloof"
|
||||
Email = "nicolas.deloof@docker.com"
|
||||
GitHub = "ndeloof"
|
||||
|
||||
[people.rumpl]
|
||||
Name = "Djordje Lukic"
|
||||
Email = "djordje.lukic@docker.com"
|
||||
GitHub = "rumpl"
|
||||
|
||||
[people.thaJeztah]
|
||||
Name = "Sebastiaan van Stijn"
|
||||
Email = "sebastiaan.vanstijn@docker.com"
|
||||
GitHub = "thaJeztah "
|
||||
|
||||
[people.StefanScherer]
|
||||
Name = "Stefan Scherer"
|
||||
Email = "stefan.scherer@docker.com"
|
||||
GitHub = "StefanScherer"
|
||||
|
||||
[people.ulyssessouza]
|
||||
Name = "Ulysses Souza"
|
||||
Email = "<ulysses.souza@docker.com"
|
||||
Github = "ulyssessouza"
|
||||
6
Makefile
6
Makefile
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
PKG := github.com/docker/compose/v2
|
||||
PKG := github.com/docker/compose/v5
|
||||
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
||||
|
||||
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
|
||||
@@ -62,11 +62,11 @@ build:
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
$(BUILDX_CMD) bake binary
|
||||
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary
|
||||
|
||||
.PHONY: binary-with-coverage
|
||||
binary-with-coverage:
|
||||
$(BUILDX_CMD) bake binary-with-coverage
|
||||
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary-with-coverage
|
||||
|
||||
.PHONY: install
|
||||
install: binary
|
||||
|
||||
15
README.md
15
README.md
@@ -1,17 +1,18 @@
|
||||
# Table of Contents
|
||||
- [Docker Compose v2](#docker-compose-v2)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Where to get Docker Compose](#where-to-get-docker-compose)
|
||||
+ [Windows and macOS](#windows-and-macos)
|
||||
+ [Linux](#linux)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Contributing](#contributing)
|
||||
- [Legacy](#legacy)
|
||||
# Docker Compose v2
|
||||
|
||||
# Docker Compose
|
||||
|
||||
[](https://github.com/docker/compose/releases/latest)
|
||||
[](https://pkg.go.dev/github.com/docker/compose/v2)
|
||||
[](https://pkg.go.dev/github.com/docker/compose/v5)
|
||||
[](https://github.com/docker/compose/actions?query=workflow%3Aci)
|
||||
[](https://goreportcard.com/report/github.com/docker/compose/v2)
|
||||
[](https://goreportcard.com/report/github.com/docker/compose/v5)
|
||||
[](https://codecov.io/gh/docker/compose)
|
||||
[](https://api.securityscorecards.dev/projects/github.com/docker/compose)
|
||||

|
||||
@@ -23,6 +24,12 @@ your application are configured.
|
||||
Once you have a Compose file, you can create and start your application with a
|
||||
single command: `docker compose up`.
|
||||
|
||||
> **Note**: About Docker Swarm
|
||||
> Docker Swarm used to rely on the legacy compose file format but did not adopt the compose specification
|
||||
> so is missing some of the recent enhancements in the compose syntax. After
|
||||
> [acquisition by Mirantis](https://www.mirantis.com/software/swarm/) swarm isn't maintained by Docker Inc, and
|
||||
> as such some Docker Compose features aren't accessible to swarm users.
|
||||
|
||||
# Where to get Docker Compose
|
||||
|
||||
### Windows and macOS
|
||||
|
||||
@@ -26,14 +26,15 @@ import (
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
commands "github.com/docker/compose/v5/cmd/compose"
|
||||
"github.com/docker/compose/v5/internal/tracing"
|
||||
)
|
||||
|
||||
// Setup should be called as part of the command's PersistentPreRunE
|
||||
@@ -55,8 +56,10 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
|
||||
ctx,
|
||||
"cli/"+strings.Join(commandName(cmd), "-"),
|
||||
)
|
||||
cmdSpan.SetAttributes(attribute.StringSlice("cli.flags", getFlags(cmd.Flags())))
|
||||
cmdSpan.SetAttributes(attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()))
|
||||
cmdSpan.SetAttributes(
|
||||
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
|
||||
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
|
||||
)
|
||||
|
||||
cmd.SetContext(ctx)
|
||||
wrapRunE(cmd, cmdSpan, tracingShutdown)
|
||||
|
||||
@@ -20,9 +20,10 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
commands "github.com/docker/compose/v5/cmd/compose"
|
||||
)
|
||||
|
||||
func TestGetFlags(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v5/cmd/compose"
|
||||
)
|
||||
|
||||
func getCompletionCommands() []string {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
@@ -16,12 +15,11 @@ package compose
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// alphaCommand groups all experimental subcommands
|
||||
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Short: "Experimental commands",
|
||||
Use: "alpha [COMMAND]",
|
||||
@@ -31,9 +29,9 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
vizCommand(p, dockerCli, backend),
|
||||
publishCommand(p, dockerCli, backend),
|
||||
generateCommand(p, backend),
|
||||
vizCommand(p, dockerCli, backendOptions),
|
||||
publishCommand(p, dockerCli, backendOptions),
|
||||
generateCommand(p, dockerCli, backendOptions),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type attachOpts struct {
|
||||
@@ -35,7 +37,7 @@ type attachOpts struct {
|
||||
proxy bool
|
||||
}
|
||||
|
||||
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := attachOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
@@ -50,7 +52,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runAttach(ctx, dockerCli, backend, opts)
|
||||
return runAttach(ctx, dockerCli, backendOptions, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -63,7 +65,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
|
||||
func runAttach(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts attachOpts) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -76,5 +78,9 @@ func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
NoStdin: opts.noStdin,
|
||||
Proxy: opts.proxy,
|
||||
}
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Attach(ctx, projectName, attachOpts)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ import (
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/bridge"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/bridge"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
@@ -62,7 +63,12 @@ func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
|
||||
project, _, err := p.ToProject(ctx, dockerCli, nil)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := p.ToProject(ctx, dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/cmd/display"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type buildOptions struct {
|
||||
@@ -45,7 +46,8 @@ type buildOptions struct {
|
||||
deps bool
|
||||
print bool
|
||||
check bool
|
||||
provenance bool
|
||||
sbom string
|
||||
provenance string
|
||||
}
|
||||
|
||||
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
|
||||
@@ -65,8 +67,8 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
||||
builderName = os.Getenv("BUILDX_BUILDER")
|
||||
}
|
||||
|
||||
uiMode := ui.Mode
|
||||
if uiMode == ui.ModeJSON {
|
||||
uiMode := display.Mode
|
||||
if uiMode == display.ModeJSON {
|
||||
uiMode = "rawjson"
|
||||
}
|
||||
|
||||
@@ -84,11 +86,12 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
||||
Check: opts.check,
|
||||
SSHs: SSHKeys,
|
||||
Builder: builderName,
|
||||
SBOM: opts.sbom,
|
||||
Provenance: opts.provenance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -97,7 +100,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Short: "Build or rebuild services",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.quiet {
|
||||
ui.Mode = ui.ModeQuiet
|
||||
display.Mode = display.ModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -113,18 +116,20 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
if cmd.Flags().Changed("progress") && opts.ssh == "" {
|
||||
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...\n")
|
||||
}
|
||||
return runBuild(ctx, dockerCli, backend, opts, args)
|
||||
return runBuild(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.push, "push", false, "Push service images")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output")
|
||||
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image")
|
||||
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services")
|
||||
flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
|
||||
flags.StringVar(&opts.builder, "builder", "", "Set builder to use")
|
||||
flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)")
|
||||
flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`)
|
||||
flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`)
|
||||
|
||||
flags.Bool("parallel", true, "Build images in parallel. DEPRECATED")
|
||||
flags.MarkHidden("parallel") //nolint:errcheck
|
||||
@@ -144,9 +149,17 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
|
||||
if opts.print {
|
||||
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
|
||||
}
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -156,10 +169,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
|
||||
}
|
||||
|
||||
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
||||
apiBuildOptions.Provenance = true
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiBuildOptions.Attestations = true
|
||||
|
||||
return backend.Build(ctx, project, apiBuildOptions)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type commitOptions struct {
|
||||
@@ -39,7 +41,7 @@ type commitOptions struct {
|
||||
index int
|
||||
}
|
||||
|
||||
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
options := commitOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -56,7 +58,7 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runCommit(ctx, dockerCli, backend, options)
|
||||
return runCommit(ctx, dockerCli, backendOptions, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -73,13 +75,17 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error {
|
||||
func runCommit(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options commitOptions) error {
|
||||
projectName, err := options.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitOptions := api.CommitOptions{
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Commit(ctx, projectName, api.CommitOptions{
|
||||
Service: options.service,
|
||||
Reference: options.reference,
|
||||
Pause: options.pause,
|
||||
@@ -87,7 +93,5 @@ func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
Author: options.author,
|
||||
Changes: options.changes,
|
||||
Index: options.index,
|
||||
}
|
||||
|
||||
return backend.Commit(ctx, projectName, commitOptions)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
// validArgsFn defines a completion func to be returned to fetch completion options
|
||||
@@ -37,7 +39,12 @@ func noCompletion() validArgsFn {
|
||||
func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
p.Offline = true
|
||||
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
@@ -52,8 +59,13 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
|
||||
}
|
||||
}
|
||||
|
||||
func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
list, err := backend.List(cmd.Context(), api.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
@@ -73,7 +85,12 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s
|
||||
func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
p.Offline = true
|
||||
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
@@ -32,25 +32,26 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
composepaths "github.com/compose-spec/compose-go/v2/paths"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
composegoutils "github.com/compose-spec/compose-go/v2/utils"
|
||||
"github.com/docker/buildx/util/logutil"
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/pkg/kvfile"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/desktop"
|
||||
"github.com/docker/compose/v2/internal/experimental"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/remote"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v5/cmd/display"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/internal/tracing"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
"github.com/docker/compose/v5/pkg/remote"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,7 +60,7 @@ const (
|
||||
// ComposeProjectName define the project name to be used, instead of guessing from parent directory
|
||||
ComposeProjectName = "COMPOSE_PROJECT_NAME"
|
||||
// ComposeCompatibility try to mimic compose v1 as much as possible
|
||||
ComposeCompatibility = "COMPOSE_COMPATIBILITY"
|
||||
ComposeCompatibility = api.ComposeCompatibility
|
||||
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
|
||||
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
|
||||
// ComposeIgnoreOrphans ignore "orphaned" containers
|
||||
@@ -85,18 +86,16 @@ func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(ke
|
||||
return nil
|
||||
}
|
||||
|
||||
var stdioToStdout bool
|
||||
|
||||
func init() {
|
||||
// compose evaluates env file values for interpolation
|
||||
// `raw` format allows to load env_file with the same parser used by docker run --env-file
|
||||
dotenv.RegisterFormat("raw", rawEnv)
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
api.Service
|
||||
|
||||
SetDesktopClient(cli *desktop.Client)
|
||||
|
||||
SetExperiments(experiments *experimental.State)
|
||||
if v, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT"); ok {
|
||||
stdioToStdout, _ = strconv.ParseBool(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Command defines a compose CLI command as a func with args
|
||||
@@ -125,7 +124,7 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
|
||||
StatusCode: 130,
|
||||
}
|
||||
}
|
||||
if ui.Mode == ui.ModeJSON {
|
||||
if display.Mode == display.ModeJSON {
|
||||
err = makeJSONError(err)
|
||||
}
|
||||
return err
|
||||
@@ -140,16 +139,17 @@ func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
type ProjectOptions struct {
|
||||
ProjectName string
|
||||
Profiles []string
|
||||
ConfigPaths []string
|
||||
WorkDir string
|
||||
ProjectDir string
|
||||
EnvFiles []string
|
||||
Compatibility bool
|
||||
Progress string
|
||||
Offline bool
|
||||
All bool
|
||||
ProjectName string
|
||||
Profiles []string
|
||||
ConfigPaths []string
|
||||
WorkDir string
|
||||
ProjectDir string
|
||||
EnvFiles []string
|
||||
Compatibility bool
|
||||
Progress string
|
||||
Offline bool
|
||||
All bool
|
||||
insecureRegistries []string
|
||||
}
|
||||
|
||||
// ProjectFunc does stuff within a types.Project
|
||||
@@ -167,13 +167,13 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func
|
||||
|
||||
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
|
||||
func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
|
||||
return Adapt(func(ctx context.Context, args []string) error {
|
||||
options := []cli.ProjectOptionsFn{
|
||||
cli.WithResolvedPaths(true),
|
||||
cli.WithoutEnvironmentResolution,
|
||||
return Adapt(func(ctx context.Context, services []string) error {
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
|
||||
project, metrics, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
|
||||
return err
|
||||
}
|
||||
|
||||
return fn(ctx, project, args)
|
||||
return fn(ctx, project, services)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -225,6 +225,8 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
|
||||
f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
|
||||
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
|
||||
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
|
||||
f.StringArrayVar(&o.insecureRegistries, "insecure-registry", []string{}, "Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images")
|
||||
_ = f.MarkHidden("insecure-registry")
|
||||
f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file")
|
||||
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
@@ -245,7 +247,12 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
|
||||
name := o.ProjectName
|
||||
var project *types.Project
|
||||
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
|
||||
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
p, _, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
envProjectName := os.Getenv(ComposeProjectName)
|
||||
if envProjectName != "" {
|
||||
@@ -269,7 +276,12 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl
|
||||
return envProjectName, nil
|
||||
}
|
||||
|
||||
project, _, err := o.ToProject(ctx, dockerCli, nil)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
project, _, err := o.ToProject(ctx, dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -294,19 +306,14 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
|
||||
return options.LoadModel(ctx)
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
|
||||
// ToProject loads a Compose project using the LoadProject API.
|
||||
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
|
||||
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
|
||||
var metrics tracing.Metrics
|
||||
remotes := o.remoteLoaders(dockerCli)
|
||||
for _, r := range remotes {
|
||||
po = append(po, cli.WithResourceLoader(r))
|
||||
}
|
||||
|
||||
options, err := o.toProjectOptions(po...)
|
||||
if err != nil {
|
||||
return nil, metrics, err
|
||||
}
|
||||
|
||||
options.WithListeners(func(event string, metadata map[string]any) {
|
||||
// Setup metrics listener to collect project data
|
||||
metricsListener := func(event string, metadata map[string]any) {
|
||||
switch event {
|
||||
case "extends":
|
||||
metrics.CountExtends++
|
||||
@@ -327,50 +334,31 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
|
||||
api.Separator = "_"
|
||||
}
|
||||
|
||||
project, err := options.LoadProject(ctx)
|
||||
loadOpts := api.ProjectLoadOptions{
|
||||
ProjectName: o.ProjectName,
|
||||
ConfigPaths: o.ConfigPaths,
|
||||
WorkingDir: o.ProjectDir,
|
||||
EnvFiles: o.EnvFiles,
|
||||
Profiles: o.Profiles,
|
||||
Services: services,
|
||||
Offline: o.Offline,
|
||||
All: o.All,
|
||||
Compatibility: o.Compatibility,
|
||||
ProjectOptionsFns: po,
|
||||
LoadListeners: []api.LoadListener{metricsListener},
|
||||
OCI: api.OCIOptions{
|
||||
InsecureRegistries: o.insecureRegistries,
|
||||
},
|
||||
}
|
||||
|
||||
project, err := backend.LoadProject(ctx, loadOpts)
|
||||
if err != nil {
|
||||
return nil, metrics, err
|
||||
}
|
||||
|
||||
if project.Name == "" {
|
||||
return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
|
||||
}
|
||||
|
||||
project, err = project.WithServicesEnabled(services...)
|
||||
if err != nil {
|
||||
return nil, metrics, err
|
||||
}
|
||||
|
||||
for name, s := range project.Services {
|
||||
s.CustomLabels = map[string]string{
|
||||
api.ProjectLabel: project.Name,
|
||||
api.ServiceLabel: 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 len(o.EnvFiles) != 0 {
|
||||
s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
|
||||
}
|
||||
project.Services[name] = s
|
||||
}
|
||||
|
||||
project, err = project.WithSelectedServices(services)
|
||||
if err != nil {
|
||||
return nil, tracing.Metrics{}, err
|
||||
}
|
||||
|
||||
if !o.All {
|
||||
project = project.WithoutUnnecessaryResources()
|
||||
}
|
||||
return project, metrics, err
|
||||
return project, metrics, nil
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
|
||||
@@ -378,37 +366,43 @@ func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceL
|
||||
return nil
|
||||
}
|
||||
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
|
||||
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
|
||||
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline, api.OCIOptions{})
|
||||
return []loader.ResourceLoader{git, oci}
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
opts := []cli.ProjectOptionsFn{
|
||||
cli.WithWorkingDirectory(o.ProjectDir),
|
||||
// First apply os.Environment, always win
|
||||
cli.WithOsEnv,
|
||||
}
|
||||
|
||||
return cli.NewProjectOptions(o.ConfigPaths,
|
||||
append(po,
|
||||
cli.WithWorkingDirectory(o.ProjectDir),
|
||||
// First apply os.Environment, always win
|
||||
cli.WithOsEnv,
|
||||
// set PWD as this variable is not consistently supported on Windows
|
||||
cli.WithEnv([]string{"PWD=" + pwd}),
|
||||
// Load PWD/.env if present and no explicit --env-file has been set
|
||||
cli.WithEnvFiles(o.EnvFiles...),
|
||||
// read dot env file to populate project environment
|
||||
cli.WithDotEnv,
|
||||
// get compose file path set by COMPOSE_FILE
|
||||
cli.WithConfigFileEnv,
|
||||
// if none was selected, get default compose.yaml file from current dir or parent folder
|
||||
cli.WithDefaultConfigPath,
|
||||
// .. and then, a project directory != PWD maybe has been set so let's load .env file
|
||||
cli.WithEnvFiles(o.EnvFiles...),
|
||||
cli.WithDotEnv,
|
||||
// eventually COMPOSE_PROFILES should have been set
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithName(o.ProjectName))...)
|
||||
if _, present := os.LookupEnv("PWD"); !present {
|
||||
if pwd, err := os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
|
||||
}
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
// Load PWD/.env if present and no explicit --env-file has been set
|
||||
cli.WithEnvFiles(o.EnvFiles...),
|
||||
// read dot env file to populate project environment
|
||||
cli.WithDotEnv,
|
||||
// get compose file path set by COMPOSE_FILE
|
||||
cli.WithConfigFileEnv,
|
||||
// if none was selected, get default compose.yaml file from current dir or parent folder
|
||||
cli.WithDefaultConfigPath,
|
||||
// .. and then, a project directory != PWD maybe has been set so let's load .env file
|
||||
cli.WithEnvFiles(o.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
|
||||
cli.WithDotEnv, //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
|
||||
// eventually COMPOSE_PROFILES should have been set
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithName(o.ProjectName),
|
||||
)
|
||||
|
||||
return cli.NewProjectOptions(o.ConfigPaths, append(po, opts...)...)
|
||||
}
|
||||
|
||||
// PluginName is the name of the plugin
|
||||
@@ -416,11 +410,19 @@ 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
|
||||
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
|
||||
}
|
||||
|
||||
type BackendOptions struct {
|
||||
Options []compose.Option
|
||||
}
|
||||
|
||||
func (o *BackendOptions) Add(option compose.Option) {
|
||||
o.Options = append(o.Options, option)
|
||||
}
|
||||
|
||||
// RootCommand returns the compose command with its child commands
|
||||
func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //nolint:gocyclo
|
||||
func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
|
||||
// filter out useless commandConn.CloseWrite warning message that can occur
|
||||
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
|
||||
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
|
||||
@@ -431,7 +433,6 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
"commandConn.CloseRead:",
|
||||
))
|
||||
|
||||
experiments := experimental.NewState()
|
||||
opts := ProjectOptions{}
|
||||
var (
|
||||
ansi string
|
||||
@@ -461,9 +462,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
}
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
parent := cmd.Root()
|
||||
|
||||
if parent != nil {
|
||||
parentPrerun := parent.PersistentPreRunE
|
||||
if parentPrerun != nil {
|
||||
@@ -478,7 +477,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts)
|
||||
err := setEnvWithDotEnv(opts, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -495,40 +494,54 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
formatter.SetANSIMode(dockerCli, ansi)
|
||||
|
||||
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
|
||||
ui.NoColor()
|
||||
display.NoColor()
|
||||
formatter.SetANSIMode(dockerCli, formatter.Never)
|
||||
}
|
||||
|
||||
switch ansi {
|
||||
case "never":
|
||||
ui.Mode = ui.ModePlain
|
||||
display.Mode = display.ModePlain
|
||||
case "always":
|
||||
ui.Mode = ui.ModeTTY
|
||||
display.Mode = display.ModeTTY
|
||||
}
|
||||
|
||||
detached, _ := cmd.Flags().GetBool("detach")
|
||||
var ep api.EventProcessor
|
||||
switch opts.Progress {
|
||||
case "", ui.ModeAuto:
|
||||
if ansi == "never" {
|
||||
ui.Mode = ui.ModePlain
|
||||
case "", display.ModeAuto:
|
||||
switch {
|
||||
case ansi == "never":
|
||||
display.Mode = display.ModePlain
|
||||
ep = display.Plain(dockerCli.Err())
|
||||
case dockerCli.Out().IsTerminal():
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
|
||||
default:
|
||||
ep = display.Plain(dockerCli.Err())
|
||||
}
|
||||
case ui.ModeTTY:
|
||||
case display.ModeTTY:
|
||||
if ansi == "never" {
|
||||
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
|
||||
}
|
||||
ui.Mode = ui.ModeTTY
|
||||
case ui.ModePlain:
|
||||
display.Mode = display.ModeTTY
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
|
||||
|
||||
case display.ModePlain:
|
||||
if ansi == "always" {
|
||||
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
|
||||
}
|
||||
ui.Mode = ui.ModePlain
|
||||
case ui.ModeQuiet, "none":
|
||||
ui.Mode = ui.ModeQuiet
|
||||
case ui.ModeJSON:
|
||||
ui.Mode = ui.ModeJSON
|
||||
display.Mode = display.ModePlain
|
||||
ep = display.Plain(dockerCli.Err())
|
||||
case display.ModeQuiet, "none":
|
||||
display.Mode = display.ModeQuiet
|
||||
ep = display.Quiet()
|
||||
case display.ModeJSON:
|
||||
display.Mode = display.ModeJSON
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||
ep = display.JSON(dockerCli.Err())
|
||||
default:
|
||||
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
|
||||
}
|
||||
backendOptions.Add(compose.WithEventProcessor(ep))
|
||||
|
||||
// (4) options validation / normalization
|
||||
if opts.WorkDir != "" {
|
||||
@@ -539,12 +552,15 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
|
||||
}
|
||||
for i, file := range opts.EnvFiles {
|
||||
file = composepaths.ExpandUser(file)
|
||||
if !filepath.IsAbs(file) {
|
||||
file, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.EnvFiles[i] = file
|
||||
} else {
|
||||
opts.EnvFiles[i] = file
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,85 +581,61 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
}
|
||||
if parallel > 0 {
|
||||
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
|
||||
backend.MaxConcurrency(parallel)
|
||||
backendOptions.Add(compose.WithMaxConcurrency(parallel))
|
||||
}
|
||||
|
||||
// dry run detection
|
||||
ctx, err = backend.DryRunMode(ctx, dryRun)
|
||||
if err != nil {
|
||||
return err
|
||||
if dryRun {
|
||||
backendOptions.Add(compose.WithDryRun)
|
||||
}
|
||||
cmd.SetContext(ctx)
|
||||
|
||||
// (6) Desktop integration
|
||||
var desktopCli *desktop.Client
|
||||
if !dryRun {
|
||||
if desktopCli, err = desktop.NewFromDockerClient(ctx, dockerCli); desktopCli != nil {
|
||||
logrus.Debugf("Enabled Docker Desktop integration (experimental) @ %s", desktopCli.Endpoint())
|
||||
backend.SetDesktopClient(desktopCli)
|
||||
} else if err != nil {
|
||||
// not fatal, Compose will still work but behave as though
|
||||
// it's not running as part of Docker Desktop
|
||||
logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
|
||||
} else {
|
||||
logrus.Trace("Docker Desktop integration not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// (7) experimental features
|
||||
if err := experiments.Load(ctx, desktopCli); err != nil {
|
||||
logrus.Debugf("Failed to query feature flags from Desktop: %v", err)
|
||||
}
|
||||
backend.SetExperiments(experiments)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
c.AddCommand(
|
||||
upCommand(&opts, dockerCli, backend),
|
||||
downCommand(&opts, dockerCli, backend),
|
||||
startCommand(&opts, dockerCli, backend),
|
||||
restartCommand(&opts, dockerCli, backend),
|
||||
stopCommand(&opts, dockerCli, backend),
|
||||
psCommand(&opts, dockerCli, backend),
|
||||
listCommand(dockerCli, backend),
|
||||
logsCommand(&opts, dockerCli, backend),
|
||||
upCommand(&opts, dockerCli, backendOptions),
|
||||
downCommand(&opts, dockerCli, backendOptions),
|
||||
startCommand(&opts, dockerCli, backendOptions),
|
||||
restartCommand(&opts, dockerCli, backendOptions),
|
||||
stopCommand(&opts, dockerCli, backendOptions),
|
||||
psCommand(&opts, dockerCli, backendOptions),
|
||||
listCommand(dockerCli, backendOptions),
|
||||
logsCommand(&opts, dockerCli, backendOptions),
|
||||
configCommand(&opts, dockerCli),
|
||||
killCommand(&opts, dockerCli, backend),
|
||||
runCommand(&opts, dockerCli, backend),
|
||||
removeCommand(&opts, dockerCli, backend),
|
||||
execCommand(&opts, dockerCli, backend),
|
||||
attachCommand(&opts, dockerCli, backend),
|
||||
exportCommand(&opts, dockerCli, backend),
|
||||
commitCommand(&opts, dockerCli, backend),
|
||||
pauseCommand(&opts, dockerCli, backend),
|
||||
unpauseCommand(&opts, dockerCli, backend),
|
||||
topCommand(&opts, dockerCli, backend),
|
||||
eventsCommand(&opts, dockerCli, backend),
|
||||
portCommand(&opts, dockerCli, backend),
|
||||
imagesCommand(&opts, dockerCli, backend),
|
||||
killCommand(&opts, dockerCli, backendOptions),
|
||||
runCommand(&opts, dockerCli, backendOptions),
|
||||
removeCommand(&opts, dockerCli, backendOptions),
|
||||
execCommand(&opts, dockerCli, backendOptions),
|
||||
attachCommand(&opts, dockerCli, backendOptions),
|
||||
exportCommand(&opts, dockerCli, backendOptions),
|
||||
commitCommand(&opts, dockerCli, backendOptions),
|
||||
pauseCommand(&opts, dockerCli, backendOptions),
|
||||
unpauseCommand(&opts, dockerCli, backendOptions),
|
||||
topCommand(&opts, dockerCli, backendOptions),
|
||||
eventsCommand(&opts, dockerCli, backendOptions),
|
||||
portCommand(&opts, dockerCli, backendOptions),
|
||||
imagesCommand(&opts, dockerCli, backendOptions),
|
||||
versionCommand(dockerCli),
|
||||
buildCommand(&opts, dockerCli, backend),
|
||||
pushCommand(&opts, dockerCli, backend),
|
||||
pullCommand(&opts, dockerCli, backend),
|
||||
createCommand(&opts, dockerCli, backend),
|
||||
copyCommand(&opts, dockerCli, backend),
|
||||
waitCommand(&opts, dockerCli, backend),
|
||||
scaleCommand(&opts, dockerCli, backend),
|
||||
buildCommand(&opts, dockerCli, backendOptions),
|
||||
pushCommand(&opts, dockerCli, backendOptions),
|
||||
pullCommand(&opts, dockerCli, backendOptions),
|
||||
createCommand(&opts, dockerCli, backendOptions),
|
||||
copyCommand(&opts, dockerCli, backendOptions),
|
||||
waitCommand(&opts, dockerCli, backendOptions),
|
||||
scaleCommand(&opts, dockerCli, backendOptions),
|
||||
statsCommand(&opts, dockerCli),
|
||||
watchCommand(&opts, dockerCli, backend),
|
||||
publishCommand(&opts, dockerCli, backend),
|
||||
alphaCommand(&opts, dockerCli, backend),
|
||||
watchCommand(&opts, dockerCli, backendOptions),
|
||||
publishCommand(&opts, dockerCli, backendOptions),
|
||||
alphaCommand(&opts, dockerCli, backendOptions),
|
||||
bridgeCommand(&opts, dockerCli),
|
||||
volumesCommand(&opts, dockerCli, backend),
|
||||
volumesCommand(&opts, dockerCli, backendOptions),
|
||||
)
|
||||
|
||||
c.Flags().SetInterspersed(false)
|
||||
opts.addProjectFlags(c.Flags())
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"project-name",
|
||||
completeProjectNames(backend),
|
||||
completeProjectNames(dockerCli, backendOptions),
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"project-directory",
|
||||
@@ -661,6 +653,10 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
"profile",
|
||||
completeProfileNames(dockerCli, &opts),
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"progress",
|
||||
cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
|
||||
)
|
||||
|
||||
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
c.Flags().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
|
||||
@@ -674,7 +670,28 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
return c
|
||||
}
|
||||
|
||||
func setEnvWithDotEnv(opts ProjectOptions) error {
|
||||
func stdinfo(dockerCli command.Cli) io.Writer {
|
||||
if stdioToStdout {
|
||||
return dockerCli.Out()
|
||||
}
|
||||
return dockerCli.Err()
|
||||
}
|
||||
|
||||
func setEnvWithDotEnv(opts ProjectOptions, dockerCli command.Cli) error {
|
||||
// Check if we're using a remote config (OCI or Git)
|
||||
// If so, skip env loading as remote loaders haven't been initialized yet
|
||||
// and trying to process the path would fail
|
||||
remoteLoaders := opts.remoteLoaders(dockerCli)
|
||||
for _, path := range opts.ConfigPaths {
|
||||
for _, loader := range remoteLoaders {
|
||||
if loader.Accept(path) {
|
||||
// Remote config - skip env loading for now
|
||||
// It will be loaded later when the project is fully initialized
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options, err := cli.NewProjectOptions(opts.ConfigPaths,
|
||||
cli.WithWorkingDirectory(opts.ProjectDir),
|
||||
cli.WithOsEnv,
|
||||
@@ -682,38 +699,26 @@ func setEnvWithDotEnv(opts ProjectOptions) error {
|
||||
cli.WithDotEnv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
for k, v := range envFromFile {
|
||||
if _, ok := os.LookupEnv(k); !ok {
|
||||
if err = os.Setenv(k, v); err != nil {
|
||||
return nil
|
||||
if _, ok := os.LookupEnv(k); !ok && strings.HasPrefix(k, "COMPOSE_") {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
var printerModes = []string{
|
||||
ui.ModeAuto,
|
||||
ui.ModeTTY,
|
||||
ui.ModePlain,
|
||||
ui.ModeJSON,
|
||||
ui.ModeQuiet,
|
||||
}
|
||||
|
||||
func SetUnchangedOption(name string, experimentalFlag bool) bool {
|
||||
var value bool
|
||||
// If the var is defined we use that value first
|
||||
if envVar, ok := os.LookupEnv(name); ok {
|
||||
value = utils.StringToBool(envVar)
|
||||
} else {
|
||||
// if not, we try to get it from experimental feature flag
|
||||
value = experimentalFlag
|
||||
}
|
||||
return value
|
||||
display.ModeAuto,
|
||||
display.ModeTTY,
|
||||
display.ModePlain,
|
||||
display.ModeJSON,
|
||||
display.ModeQuiet,
|
||||
}
|
||||
|
||||
76
cmd/compose/compose_oci_test.go
Normal file
76
cmd/compose/compose_oci_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestSetEnvWithDotEnv_WithOCIArtifact(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv doesn't fail when using OCI artifacts
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"oci://docker.io/dockersamples/welcome-to-docker"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts, cli)
|
||||
assert.NilError(t, err, "setEnvWithDotEnv should not fail with OCI artifact path")
|
||||
}
|
||||
|
||||
func TestSetEnvWithDotEnv_WithGitRemote(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv doesn't fail when using Git remotes
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"https://github.com/docker/compose.git"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts, cli)
|
||||
assert.NilError(t, err, "setEnvWithDotEnv should not fail with Git remote path")
|
||||
}
|
||||
|
||||
func TestSetEnvWithDotEnv_WithLocalPath(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv still works with local paths
|
||||
// This will fail if the file doesn't exist, but it should not panic
|
||||
// or produce invalid paths
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"compose.yaml"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
// This may error if files don't exist, but should not panic
|
||||
_ = setEnvWithDotEnv(opts, cli)
|
||||
}
|
||||
@@ -30,12 +30,12 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/template"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type configOptions struct {
|
||||
@@ -51,6 +51,7 @@ type configOptions struct {
|
||||
services bool
|
||||
volumes bool
|
||||
networks bool
|
||||
models bool
|
||||
profiles bool
|
||||
images bool
|
||||
hash string
|
||||
@@ -60,19 +61,19 @@ type configOptions struct {
|
||||
lockImageDigests bool
|
||||
}
|
||||
|
||||
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
|
||||
po = append(po, o.ToProjectOptions()...)
|
||||
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...)
|
||||
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string) (*types.Project, error) {
|
||||
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, o.toProjectOptionsFns()...)
|
||||
return project, err
|
||||
}
|
||||
|
||||
func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
|
||||
po = append(po, o.ToProjectOptions()...)
|
||||
po = append(po, o.toProjectOptionsFns()...)
|
||||
return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
|
||||
}
|
||||
|
||||
func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
|
||||
return []cli.ProjectOptionsFn{
|
||||
// toProjectOptionsFns converts config options to cli.ProjectOptionsFn
|
||||
func (o *configOptions) toProjectOptionsFns() []cli.ProjectOptionsFn {
|
||||
fns := []cli.ProjectOptionsFn{
|
||||
cli.WithInterpolation(!o.noInterpolate),
|
||||
cli.WithResolvedPaths(!o.noResolvePath),
|
||||
cli.WithNormalization(!o.noNormalize),
|
||||
@@ -80,6 +81,10 @@ func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithDiscardEnvFile,
|
||||
}
|
||||
if o.noResolveEnv {
|
||||
fns = append(fns, cli.WithoutEnvironmentResolution)
|
||||
}
|
||||
return fns
|
||||
}
|
||||
|
||||
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
@@ -115,6 +120,9 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
if opts.networks {
|
||||
return runNetworks(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.models {
|
||||
return runModels(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.hash != "" {
|
||||
return runHash(ctx, dockerCli, opts)
|
||||
}
|
||||
@@ -152,6 +160,7 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
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.networks, "networks", false, "Print the network names, one per line.")
|
||||
flags.BoolVar(&opts.models, "models", false, "Print the model 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.")
|
||||
@@ -192,7 +201,12 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s
|
||||
}
|
||||
|
||||
func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -319,17 +333,16 @@ func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[s
|
||||
func formatModel(model map[string]any, format string) (content []byte, err error) {
|
||||
switch format {
|
||||
case "json":
|
||||
content, err = json.MarshalIndent(model, "", " ")
|
||||
return json.MarshalIndent(model, "", " ")
|
||||
case "yaml":
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
encoder := yaml.NewEncoder(buf)
|
||||
encoder.SetIndent(2)
|
||||
err = encoder.Encode(model)
|
||||
content = buf.Bytes()
|
||||
return buf.Bytes(), err
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format %q", format)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
@@ -349,7 +362,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -362,7 +380,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
|
||||
}
|
||||
|
||||
func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -373,7 +396,12 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
|
||||
}
|
||||
|
||||
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -383,12 +411,36 @@ func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, model := range project.Models {
|
||||
if model.Model != "" {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
var services []string
|
||||
if opts.hash != "*" {
|
||||
services = append(services, strings.Split(opts.hash, ",")...)
|
||||
}
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -423,7 +475,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
|
||||
|
||||
func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
set := map[string]struct{}{}
|
||||
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -444,7 +502,12 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions,
|
||||
}
|
||||
|
||||
func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -481,7 +544,12 @@ func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions
|
||||
}
|
||||
|
||||
func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services)
|
||||
backend, err := compose.NewComposeService(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type copyOptions struct {
|
||||
@@ -38,7 +39,7 @@ type copyOptions struct {
|
||||
copyUIDGID bool
|
||||
}
|
||||
|
||||
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := copyOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.source = args[0]
|
||||
opts.destination = args[1]
|
||||
return runCopy(ctx, dockerCli, backend, opts)
|
||||
return runCopy(ctx, dockerCli, backendOptions, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -73,12 +74,16 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return copyCmd
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
|
||||
func runCopy(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts copyOptions) error {
|
||||
name, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Copy(ctx, name, api.CopyOptions{
|
||||
Source: opts.source,
|
||||
Destination: opts.destination,
|
||||
|
||||
@@ -30,7 +30,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
@@ -51,7 +52,7 @@ type createOptions struct {
|
||||
AssumeYes bool
|
||||
}
|
||||
|
||||
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func createCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := createOptions{}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
@@ -70,7 +71,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return nil
|
||||
}),
|
||||
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
|
||||
return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
|
||||
return runCreate(ctx, dockerCli, backendOptions, opts, buildOpts, project, services)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -95,7 +96,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
|
||||
func runCreate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
|
||||
if err := createOpts.Apply(project); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,6 +110,14 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
|
||||
build = &bo
|
||||
}
|
||||
|
||||
if createOpts.AssumeYes {
|
||||
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Create(ctx, project, api.CreateOptions{
|
||||
Build: build,
|
||||
Services: services,
|
||||
@@ -119,7 +128,6 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
|
||||
Inherit: !createOpts.noInherit,
|
||||
Timeout: createOpts.GetTimeout(),
|
||||
QuietPull: createOpts.quietPull,
|
||||
AssumeYes: createOpts.AssumeYes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,12 +198,11 @@ func (opts createOptions) Apply(project *types.Project) error {
|
||||
|
||||
func applyScaleOpts(project *types.Project, opts []string) error {
|
||||
for _, scale := range opts {
|
||||
split := strings.Split(scale, "=")
|
||||
if len(split) != 2 {
|
||||
name, val, ok := strings.Cut(scale, "=")
|
||||
if !ok || val == "" {
|
||||
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
|
||||
}
|
||||
name := split[0]
|
||||
replicas, err := strconv.Atoi(split[1])
|
||||
replicas, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 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"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestRunCreate(t *testing.T) {
|
||||
ctrl, ctx := gomock.WithContext(context.Background(), t)
|
||||
backend := mocks.NewMockService(ctrl)
|
||||
backend.EXPECT().Create(
|
||||
gomock.Eq(ctx),
|
||||
pullPolicy(""),
|
||||
deepEqual(defaultCreateOptions(true)),
|
||||
)
|
||||
|
||||
createOpts := createOptions{}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: &ProjectOptions{},
|
||||
}
|
||||
project := sampleProject()
|
||||
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRunCreate_Build(t *testing.T) {
|
||||
ctrl, ctx := gomock.WithContext(context.Background(), t)
|
||||
backend := mocks.NewMockService(ctrl)
|
||||
backend.EXPECT().Create(
|
||||
gomock.Eq(ctx),
|
||||
pullPolicy("build"),
|
||||
deepEqual(defaultCreateOptions(true)),
|
||||
)
|
||||
|
||||
createOpts := createOptions{
|
||||
Build: true,
|
||||
}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: &ProjectOptions{},
|
||||
}
|
||||
project := sampleProject()
|
||||
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRunCreate_NoBuild(t *testing.T) {
|
||||
ctrl, ctx := gomock.WithContext(context.Background(), t)
|
||||
backend := mocks.NewMockService(ctrl)
|
||||
backend.EXPECT().Create(
|
||||
gomock.Eq(ctx),
|
||||
pullPolicy(""),
|
||||
deepEqual(defaultCreateOptions(false)),
|
||||
)
|
||||
|
||||
createOpts := createOptions{
|
||||
noBuild: true,
|
||||
}
|
||||
buildOpts := buildOptions{}
|
||||
project := sampleProject()
|
||||
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func sampleProject() *types.Project {
|
||||
return &types.Project{
|
||||
Name: "test",
|
||||
Services: types.Services{
|
||||
"svc": {
|
||||
Name: "svc",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCreateOptions(includeBuild bool) api.CreateOptions {
|
||||
var build *api.BuildOptions
|
||||
if includeBuild {
|
||||
bo := defaultBuildOptions()
|
||||
build = &bo
|
||||
}
|
||||
return api.CreateOptions{
|
||||
Build: build,
|
||||
Services: nil,
|
||||
RemoveOrphans: false,
|
||||
IgnoreOrphans: false,
|
||||
Recreate: "diverged",
|
||||
RecreateDependencies: "diverged",
|
||||
Inherit: true,
|
||||
Timeout: nil,
|
||||
QuietPull: false,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultBuildOptions() api.BuildOptions {
|
||||
return api.BuildOptions{
|
||||
Args: make(types.MappingWithEquals),
|
||||
Progress: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
// deepEqual returns a nice diff on failure vs gomock.Eq when used
|
||||
// on structs.
|
||||
func deepEqual(x interface{}) gomock.Matcher {
|
||||
return gomock.GotFormatterAdapter(
|
||||
gomock.GotFormatterFunc(func(got interface{}) string {
|
||||
return cmp.Diff(x, got)
|
||||
}),
|
||||
gomock.Eq(x),
|
||||
)
|
||||
}
|
||||
|
||||
func spewAdapter(m gomock.Matcher) gomock.Matcher {
|
||||
return gomock.GotFormatterAdapter(
|
||||
gomock.GotFormatterFunc(func(got interface{}) string {
|
||||
return spew.Sdump(got)
|
||||
}),
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
type withPullPolicy struct {
|
||||
policy string
|
||||
}
|
||||
|
||||
func pullPolicy(policy string) gomock.Matcher {
|
||||
return spewAdapter(withPullPolicy{policy: policy})
|
||||
}
|
||||
|
||||
func (w withPullPolicy) Matches(x interface{}) bool {
|
||||
proj, ok := x.(*types.Project)
|
||||
if !ok || proj == nil || len(proj.Services) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, svc := range proj.Services {
|
||||
if svc.PullPolicy != w.policy {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (w withPullPolicy) String() string {
|
||||
return fmt.Sprintf("has pull policy %q for all services", w.policy)
|
||||
}
|
||||
@@ -23,12 +23,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
type downOptions struct {
|
||||
@@ -40,7 +41,7 @@ type downOptions struct {
|
||||
images string
|
||||
}
|
||||
|
||||
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := downOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -57,9 +58,9 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runDown(ctx, dockerCli, backend, opts, args)
|
||||
return runDown(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: noCompletion(),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := downCmd.Flags()
|
||||
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
@@ -77,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return downCmd
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
|
||||
func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts downOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -88,6 +89,10 @@ func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Down(ctx, name, api.DownOptions{
|
||||
RemoveOrphans: opts.removeOrphans,
|
||||
Project: project,
|
||||
|
||||
@@ -22,17 +22,20 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type eventsOpts struct {
|
||||
*composeOptions
|
||||
json bool
|
||||
json bool
|
||||
since string
|
||||
until string
|
||||
}
|
||||
|
||||
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := eventsOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
@@ -42,26 +45,34 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
Use: "events [OPTIONS] [SERVICE...]",
|
||||
Short: "Receive real time events from containers",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runEvents(ctx, dockerCli, backend, opts, args)
|
||||
return runEvents(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
|
||||
cmd.Flags().StringVar(&opts.since, "since", "", "Show all events created since timestamp")
|
||||
cmd.Flags().StringVar(&opts.until, "until", "", "Stream events until this timestamp")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
|
||||
func runEvents(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts eventsOpts, services []string) error {
|
||||
name, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Events(ctx, name, api.EventsOptions{
|
||||
Services: services,
|
||||
Since: opts.since,
|
||||
Until: opts.until,
|
||||
Consumer: func(event api.Event) error {
|
||||
if opts.json {
|
||||
marshal, err := json.Marshal(map[string]interface{}{
|
||||
marshal, err := json.Marshal(map[string]any{
|
||||
"time": event.Timestamp,
|
||||
"type": "container",
|
||||
"service": event.Service,
|
||||
|
||||
@@ -25,10 +25,12 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type execOpts struct {
|
||||
@@ -47,7 +49,7 @@ type execOpts struct {
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func execCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := execOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
@@ -63,7 +65,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
err := runExec(ctx, dockerCli, backend, opts)
|
||||
err := runExec(ctx, dockerCli, backendOptions, opts)
|
||||
if err != nil {
|
||||
logrus.Debugf("%v", err)
|
||||
var cliError cli.StatusError
|
||||
@@ -81,7 +83,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
|
||||
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", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
|
||||
runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !dockerCli.Out().IsTerminal(), "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().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
|
||||
@@ -90,10 +92,16 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
runCmd.Flags().SetInterspersed(false)
|
||||
runCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
if name == "no-TTY" { // legacy
|
||||
name = "no-tty"
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
|
||||
func runExec(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts execOpts) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -119,6 +127,10 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
Interactive: opts.interactive,
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exitCode, err := backend.Exec(ctx, projectName, execOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := fmt.Sprintf("exit status %d", exitCode)
|
||||
|
||||
@@ -22,7 +22,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type exportOptions struct {
|
||||
@@ -33,7 +34,7 @@ type exportOptions struct {
|
||||
index int
|
||||
}
|
||||
|
||||
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
options := exportOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -46,7 +47,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runExport(ctx, dockerCli, backend, options)
|
||||
return runExport(ctx, dockerCli, backendOptions, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -58,7 +59,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options exportOptions) error {
|
||||
projectName, err := options.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -70,5 +71,9 @@ func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
Output: options.output,
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Export(ctx, projectName, exportOptions)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type generateOptions struct {
|
||||
@@ -30,7 +33,7 @@ type generateOptions struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
func generateCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := generateOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -42,7 +45,7 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runGenerate(ctx, backend, opts, args)
|
||||
return runGenerate(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -52,11 +55,16 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
|
||||
func runGenerate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts generateOptions, containers []string) error {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
|
||||
if len(containers) == 0 {
|
||||
return fmt.Errorf("at least one container must be specified")
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := backend.Generate(ctx, api.GenerateOptions{
|
||||
Containers: containers,
|
||||
ProjectName: opts.ProjectName,
|
||||
@@ -64,6 +72,7 @@ func runGenerate(ctx context.Context, backend api.Service, opts generateOptions,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content []byte
|
||||
switch opts.Format {
|
||||
case "json":
|
||||
|
||||
@@ -31,8 +31,9 @@ import (
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type imageOptions struct {
|
||||
@@ -41,7 +42,7 @@ type imageOptions struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := imageOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
Use: "images [OPTIONS] [SERVICE...]",
|
||||
Short: "List images used by the created containers",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runImages(ctx, dockerCli, backend, opts, args)
|
||||
return runImages(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -58,12 +59,16 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
return imgCmd
|
||||
}
|
||||
|
||||
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
|
||||
func runImages(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts imageOptions, services []string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images, err := backend.Images(ctx, projectName, api.ImagesOptions{
|
||||
Services: services,
|
||||
})
|
||||
@@ -90,17 +95,19 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
if opts.Format == "json" {
|
||||
|
||||
type img struct {
|
||||
ID string `json:"ID"`
|
||||
ContainerName string `json:"ContainerName"`
|
||||
Repository string `json:"Repository"`
|
||||
Tag string `json:"Tag"`
|
||||
Platform string `json:"Platform"`
|
||||
Size int64 `json:"Size"`
|
||||
LastTagTime time.Time `json:"LastTagTime"`
|
||||
ID string `json:"ID"`
|
||||
ContainerName string `json:"ContainerName"`
|
||||
Repository string `json:"Repository"`
|
||||
Tag string `json:"Tag"`
|
||||
Platform string `json:"Platform"`
|
||||
Size int64 `json:"Size"`
|
||||
Created *time.Time `json:"Created,omitempty"`
|
||||
LastTagTime time.Time `json:"LastTagTime,omitzero"`
|
||||
}
|
||||
// Convert map to slice
|
||||
var imageList []img
|
||||
for ctr, i := range images {
|
||||
lastTagTime := i.LastTagTime
|
||||
imageList = append(imageList, img{
|
||||
ContainerName: ctr,
|
||||
ID: i.ID,
|
||||
@@ -108,7 +115,8 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
Tag: i.Tag,
|
||||
Platform: platforms.Format(i.Platform),
|
||||
Size: i.Size,
|
||||
LastTagTime: i.LastTagTime,
|
||||
Created: i.Created,
|
||||
LastTagTime: lastTagTime,
|
||||
})
|
||||
}
|
||||
json, err := formatter.ToJSON(imageList, "", "")
|
||||
@@ -133,7 +141,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
if tag == "" {
|
||||
tag = "<none>"
|
||||
}
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
|
||||
created := "N/A"
|
||||
if img.Created != nil {
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(*img.Created)) + " ago"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
container, repo, tag, platforms.Format(img.Platform), id, size, created)
|
||||
}
|
||||
|
||||
@@ -18,13 +18,16 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
type killOptions struct {
|
||||
@@ -33,7 +36,7 @@ type killOptions struct {
|
||||
signal string
|
||||
}
|
||||
|
||||
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func killCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := killOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -41,7 +44,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Use: "kill [OPTIONS] [SERVICE...]",
|
||||
Short: "Force stop service containers",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runKill(ctx, dockerCli, backend, opts, args)
|
||||
return runKill(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -54,16 +57,25 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
|
||||
func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts killOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Kill(ctx, name, api.KillOptions{
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = backend.Kill(ctx, name, api.KillOptions{
|
||||
RemoveOrphans: opts.removeOrphans,
|
||||
Project: project,
|
||||
Services: services,
|
||||
Signal: opts.signal,
|
||||
})
|
||||
if errors.Is(err, api.ErrNoResources) {
|
||||
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type lsOptions struct {
|
||||
@@ -38,13 +38,13 @@ type lsOptions struct {
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func listCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
|
||||
lsCmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
Short: "List running compose projects",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runList(ctx, dockerCli, backend, lsOpts)
|
||||
return runList(ctx, dockerCli, backendOptions, lsOpts)
|
||||
}),
|
||||
Args: cobra.NoArgs,
|
||||
ValidArgsFunction: noCompletion(),
|
||||
@@ -61,13 +61,17 @@ var acceptedListFilters = map[string]bool{
|
||||
"name": true,
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error {
|
||||
func runList(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, lsOpts lsOptions) error {
|
||||
filters := lsOpts.Filter.Value()
|
||||
err := filters.Validate(acceptedListFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -23,8 +23,9 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type logsOptions struct {
|
||||
@@ -40,7 +41,7 @@ type logsOptions struct {
|
||||
timestamps bool
|
||||
}
|
||||
|
||||
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := logsOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -48,7 +49,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Use: "logs [OPTIONS] [SERVICE...]",
|
||||
Short: "View output from containers",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runLogs(ctx, dockerCli, backend, opts, args)
|
||||
return runLogs(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.index > 0 && len(args) != 1 {
|
||||
@@ -70,7 +71,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return logsCmd
|
||||
}
|
||||
|
||||
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
|
||||
func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts logsOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -85,6 +86,10 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
}
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
|
||||
return backend.Logs(ctx, name, consumer, api.LogOptions{
|
||||
Project: project,
|
||||
@@ -97,3 +102,32 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
Timestamps: opts.timestamps,
|
||||
})
|
||||
}
|
||||
|
||||
var _ api.LogConsumer = &logConsumer{}
|
||||
|
||||
type logConsumer struct {
|
||||
events api.EventProcessor
|
||||
}
|
||||
|
||||
func (l logConsumer) Log(containerName, message string) {
|
||||
l.events.On(api.Resource{
|
||||
ID: containerName,
|
||||
Text: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (l logConsumer) Err(containerName, message string) {
|
||||
l.events.On(api.Resource{
|
||||
ID: containerName,
|
||||
Status: api.Error,
|
||||
Text: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (l logConsumer) Status(containerName, message string) {
|
||||
l.events.On(api.Resource{
|
||||
ID: containerName,
|
||||
Status: api.Error,
|
||||
Text: message,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/template"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/prompt"
|
||||
|
||||
"github.com/docker/compose/v5/cmd/display"
|
||||
"github.com/docker/compose/v5/cmd/prompt"
|
||||
"github.com/docker/compose/v5/internal/tracing"
|
||||
)
|
||||
|
||||
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
@@ -212,9 +213,9 @@ func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
|
||||
// Parse command-line environment variables
|
||||
cmdEnvMap := make(map[string]string)
|
||||
for _, env := range cmdEnvs {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
cmdEnvMap[parts[0]] = parts[1]
|
||||
key, val, ok := strings.Cut(env, "=")
|
||||
if ok {
|
||||
cmdEnvMap[key] = val
|
||||
}
|
||||
}
|
||||
return cmdEnvMap
|
||||
@@ -247,7 +248,7 @@ func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
|
||||
|
||||
func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
|
||||
mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck
|
||||
if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON {
|
||||
if display.Mode != display.ModeQuiet && display.Mode != display.ModeJSON {
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -28,9 +27,10 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
|
||||
@@ -213,10 +213,7 @@ func TestDisplayInterpolationVariables(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
// Create a temporary directory for the test
|
||||
tmpDir, err := os.MkdirTemp("", "compose-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a temporary compose file
|
||||
composeContent := `
|
||||
@@ -230,8 +227,7 @@ services:
|
||||
- UNSET_VAR # optional without default
|
||||
`
|
||||
composePath := filepath.Join(tmpDir, "docker-compose.yml")
|
||||
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(composePath, []byte(composeContent), 0o644))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
@@ -243,16 +239,11 @@ services:
|
||||
}
|
||||
|
||||
// Set up the context with necessary environment variables
|
||||
ctx := context.Background()
|
||||
_ = os.Setenv("TEST_VAR", "test-value")
|
||||
_ = os.Setenv("API_KEY", "123456")
|
||||
defer func() {
|
||||
_ = os.Unsetenv("TEST_VAR")
|
||||
_ = os.Unsetenv("API_KEY")
|
||||
}()
|
||||
t.Setenv("TEST_VAR", "test-value")
|
||||
t.Setenv("API_KEY", "123456")
|
||||
|
||||
// Extract variables from the model
|
||||
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
|
||||
info, noVariables, err := extractInterpolationVariablesFromModel(t.Context(), cli, projectOptions, []string{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, noVariables)
|
||||
|
||||
|
||||
@@ -22,14 +22,15 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type pauseOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := pauseOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -37,19 +38,23 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Use: "pause [SERVICE...]",
|
||||
Short: "Pause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPause(ctx, dockerCli, backend, opts, args)
|
||||
return runPause(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
|
||||
func runPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pauseOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Pause(ctx, name, api.PauseOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
@@ -60,7 +65,7 @@ type unpauseOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := unpauseOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -68,19 +73,23 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
Use: "unpause [SERVICE...]",
|
||||
Short: "Unpause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runUnPause(ctx, dockerCli, backend, opts, args)
|
||||
return runUnPause(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
|
||||
func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts unpauseOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.UnPause(ctx, name, api.PauseOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type portOptions struct {
|
||||
@@ -35,7 +36,7 @@ type portOptions struct {
|
||||
index int
|
||||
}
|
||||
|
||||
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := portOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -53,7 +54,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPort(ctx, dockerCli, backend, opts, args[0])
|
||||
return runPort(ctx, dockerCli, backendOptions, opts, args[0])
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -62,11 +63,16 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
|
||||
func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts portOptions, service string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
|
||||
Protocol: opts.protocol,
|
||||
Index: opts.index,
|
||||
|
||||
@@ -24,13 +24,14 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliformatter "github.com/docker/cli/cli/command/formatter"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
@@ -49,22 +50,22 @@ func (p *psOptions) parseFilter() error {
|
||||
if p.Filter == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitN(p.Filter, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
key, val, ok := strings.Cut(p.Filter, "=")
|
||||
if !ok {
|
||||
return errors.New("arguments to --filter should be in form KEY=VAL")
|
||||
}
|
||||
switch parts[0] {
|
||||
switch key {
|
||||
case "status":
|
||||
p.Status = append(p.Status, parts[1])
|
||||
p.Status = append(p.Status, val)
|
||||
return nil
|
||||
case "source":
|
||||
return api.ErrNotImplemented
|
||||
default:
|
||||
return fmt.Errorf("unknown filter %s", parts[0])
|
||||
return fmt.Errorf("unknown filter %s", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := psOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -75,7 +76,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
return opts.parseFilter()
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPs(ctx, dockerCli, backend, args, opts)
|
||||
return runPs(ctx, dockerCli, backendOptions, args, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -91,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
return psCmd
|
||||
}
|
||||
|
||||
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
|
||||
func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, opts psOptions) error { //nolint:gocyclo
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -111,6 +112,10 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
|
||||
}
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers, err := backend.Ps(ctx, name, api.PsOptions{
|
||||
Project: project,
|
||||
All: opts.All || len(opts.Status) != 0,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestPsTable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dir := t.TempDir()
|
||||
out := filepath.Join(dir, "output.txt")
|
||||
f, err := os.Create(out)
|
||||
if err != nil {
|
||||
t.Fatal("could not create output file")
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
backend := mocks.NewMockService(ctrl)
|
||||
backend.EXPECT().
|
||||
Ps(gomock.Eq(ctx), gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
|
||||
return []api.ContainerSummary{
|
||||
{
|
||||
ID: "abc123",
|
||||
Name: "ABC",
|
||||
Image: "foo/bar",
|
||||
Publishers: api.PortPublishers{
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 8080,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
TargetPort: 8443,
|
||||
PublishedPort: 8443,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}).AnyTimes()
|
||||
|
||||
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
|
||||
stdout := streams.NewOut(f)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
cli.EXPECT().Out().Return(stdout).AnyTimes()
|
||||
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
|
||||
err = runPs(ctx, cli, backend, nil, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Seek(0, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
|
||||
}
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type publishOptions struct {
|
||||
@@ -34,9 +36,11 @@ type publishOptions struct {
|
||||
ociVersion string
|
||||
withEnvironment bool
|
||||
assumeYes bool
|
||||
app bool
|
||||
insecureRegistry bool
|
||||
}
|
||||
|
||||
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := publishOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -44,7 +48,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
|
||||
Short: "Publish compose application",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPublish(ctx, dockerCli, backend, opts, args[0])
|
||||
return runPublish(ctx, dockerCli, backendOptions, opts, args[0])
|
||||
}),
|
||||
Args: cli.ExactArgs(1),
|
||||
}
|
||||
@@ -53,6 +57,8 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
|
||||
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
|
||||
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
|
||||
flags.BoolVar(&opts.app, "app", false, "Published compose application (includes referenced images)")
|
||||
flags.BoolVar(&opts.insecureRegistry, "insecure-registry", false, "Use insecure registry")
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
// assumeYes was introduced by mistake as `--y`
|
||||
if name == "y" {
|
||||
@@ -61,12 +67,23 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
// Should **only** be used for testing purpose, we don't want to promote use of insecure registries
|
||||
_ = flags.MarkHidden("insecure-registry")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
|
||||
project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
|
||||
func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error {
|
||||
if opts.assumeYes {
|
||||
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,9 +93,10 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
}
|
||||
|
||||
return backend.Publish(ctx, project, repository, api.PublishOptions{
|
||||
ResolveImageDigests: opts.resolveImageDigests,
|
||||
ResolveImageDigests: opts.resolveImageDigests || opts.app,
|
||||
Application: opts.app,
|
||||
OCIVersion: api.OCIVersion(opts.ociVersion),
|
||||
WithEnvironment: opts.withEnvironment,
|
||||
AssumeYes: opts.assumeYes,
|
||||
InsecureRegistry: opts.insecureRegistry,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import (
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type pullOptions struct {
|
||||
@@ -42,7 +43,7 @@ type pullOptions struct {
|
||||
policy string
|
||||
}
|
||||
|
||||
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := pullOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return nil
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPull(ctx, dockerCli, backend, opts, args)
|
||||
return runPull(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -97,8 +98,13 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error {
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type pushOptions struct {
|
||||
@@ -34,7 +35,7 @@ type pushOptions struct {
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := pushOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -42,7 +43,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Use: "push [OPTIONS] [SERVICE...]",
|
||||
Short: "Push service images",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPush(ctx, dockerCli, backend, opts, args)
|
||||
return runPush(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -53,8 +54,13 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return pushCmd
|
||||
}
|
||||
|
||||
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services)
|
||||
func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error {
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
@@ -31,7 +35,7 @@ type removeOptions struct {
|
||||
volumes bool
|
||||
}
|
||||
|
||||
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := removeOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -45,7 +49,7 @@ can override this with -v. To list all volumes, use "docker volume ls".
|
||||
|
||||
Any data which is not in a volume will be lost.`,
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRemove(ctx, dockerCli, backend, opts, args)
|
||||
return runRemove(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -59,17 +63,26 @@ Any data which is not in a volume will be lost.`,
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts removeOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Remove(ctx, name, api.RemoveOptions{
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = backend.Remove(ctx, name, api.RemoveOptions{
|
||||
Services: services,
|
||||
Force: opts.force,
|
||||
Volumes: opts.volumes,
|
||||
Project: project,
|
||||
Stop: opts.stop,
|
||||
})
|
||||
if errors.Is(err, api.ErrNoResources) {
|
||||
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No stopped containers")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type restartOptions struct {
|
||||
@@ -33,7 +34,7 @@ type restartOptions struct {
|
||||
noDeps bool
|
||||
}
|
||||
|
||||
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := restartOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRestart(ctx, dockerCli, backend, opts, args)
|
||||
return runRestart(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -55,7 +56,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
return restartCmd
|
||||
}
|
||||
|
||||
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
|
||||
func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts restartOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -74,6 +75,10 @@ func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Restart(ctx, name, api.RestartOptions{
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
|
||||
@@ -22,23 +22,23 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/format"
|
||||
xprogress "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
cgo "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/mattn/go-shellwords"
|
||||
xprogress "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v5/cmd/display"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
type runOptions struct {
|
||||
@@ -50,7 +50,6 @@ type runOptions struct {
|
||||
Detach bool
|
||||
Remove bool
|
||||
noTty bool
|
||||
tty bool
|
||||
interactive bool
|
||||
user string
|
||||
workdir string
|
||||
@@ -120,8 +119,8 @@ func (options runOptions) apply(project *types.Project) (*types.Project, error)
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (options runOptions) getEnvironment() (types.Mapping, error) {
|
||||
environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).ToMapping()
|
||||
func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (types.Mapping, error) {
|
||||
environment := types.NewMappingWithEquals(options.environment).Resolve(resolve).ToMapping()
|
||||
for _, file := range options.envFiles {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
@@ -143,7 +142,7 @@ func (options runOptions) getEnvironment() (types.Mapping, error) {
|
||||
return environment, nil
|
||||
}
|
||||
|
||||
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
options := runOptions{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
@@ -155,6 +154,10 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
// We remove the attribute from the option struct and use a dedicated var, to limit confusion and avoid anyone to use options.tty.
|
||||
// The tty flag is here for convenience and let user do "docker compose run -it" the same way as they use the "docker run" command.
|
||||
var ttyFlag bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
|
||||
Short: "Run a one-off command on a service",
|
||||
@@ -178,22 +181,30 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
if cmd.Flags().Changed("no-TTY") {
|
||||
return fmt.Errorf("--tty and --no-TTY can't be used together")
|
||||
} else {
|
||||
options.noTty = !options.tty
|
||||
options.noTty = !ttyFlag
|
||||
}
|
||||
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !dockerCli.In().IsTerminal() {
|
||||
// while `docker run` requires explicit `-it` flags, Compose enables interactive mode and TTY by default
|
||||
// but when compose is used from a script that has stdin piped from another command, we just can't
|
||||
// Here, we detect we run "by default" (user didn't passed explicit flags) and disable TTY allocation if
|
||||
// we don't have an actual terminal to attach to for interactive mode
|
||||
options.noTty = true
|
||||
}
|
||||
|
||||
if options.quiet {
|
||||
progress.Mode = progress.ModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
display.Mode = display.ModeQuiet
|
||||
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
|
||||
}
|
||||
createOpts.pullChanged = cmd.Flags().Changed("pull")
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution)
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.Service}, composecli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -238,7 +249,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
|
||||
|
||||
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
|
||||
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY")
|
||||
cmd.Flags().BoolVarP(&ttyFlag, "tty", "t", true, "Allocate a pseudo-TTY")
|
||||
cmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
flags.SetNormalizeFunc(normalizeRunFlags)
|
||||
@@ -256,7 +267,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
|
||||
func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
|
||||
project, err := options.apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,11 +284,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
|
||||
labels := types.Labels{}
|
||||
for _, s := range options.labels {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
key, val, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("label must be set as KEY=VALUE")
|
||||
}
|
||||
labels[parts[0]] = parts[1]
|
||||
labels[key] = val
|
||||
}
|
||||
|
||||
var buildForRun *api.BuildOptions
|
||||
@@ -289,7 +300,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
buildForRun = &bo
|
||||
}
|
||||
|
||||
environment, err := options.getEnvironment()
|
||||
environment, err := options.getEnvironment(project.Environment.Resolve)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type scaleOptions struct {
|
||||
@@ -35,7 +37,7 @@ type scaleOptions struct {
|
||||
noDeps bool
|
||||
}
|
||||
|
||||
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := scaleOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -48,7 +50,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runScale(ctx, dockerCli, backend, opts, serviceTuples)
|
||||
return runScale(ctx, dockerCli, backendOptions, opts, serviceTuples)
|
||||
}),
|
||||
ValidArgsFunction: completeScaleArgs(dockerCli, p),
|
||||
}
|
||||
@@ -58,9 +60,14 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return scaleCmd
|
||||
}
|
||||
|
||||
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
|
||||
func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts scaleOptions, serviceReplicaTuples map[string]int) error {
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services)
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,17 +18,22 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type startOptions struct {
|
||||
*ProjectOptions
|
||||
wait bool
|
||||
waitTimeout int
|
||||
}
|
||||
|
||||
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func startCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := startOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -36,22 +41,37 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Use: "start [SERVICE...]",
|
||||
Short: "Start services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStart(ctx, dockerCli, backend, opts, args)
|
||||
return runStart(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := startCmd.Flags()
|
||||
flags.BoolVar(&opts.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
|
||||
flags.IntVar(&opts.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
|
||||
|
||||
return startCmd
|
||||
}
|
||||
|
||||
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
|
||||
func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts startOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
if opts.waitTimeout > 0 {
|
||||
timeout = time.Duration(opts.waitTimeout) * time.Second
|
||||
}
|
||||
return backend.Start(ctx, name, api.StartOptions{
|
||||
AttachTo: services,
|
||||
Project: project,
|
||||
Services: services,
|
||||
AttachTo: services,
|
||||
Project: project,
|
||||
Services: services,
|
||||
Wait: opts.wait,
|
||||
WaitTimeout: timeout,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
type statsOptions struct {
|
||||
|
||||
@@ -23,7 +23,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type stopOptions struct {
|
||||
@@ -32,7 +33,7 @@ type stopOptions struct {
|
||||
timeout int
|
||||
}
|
||||
|
||||
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := stopOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -43,7 +44,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStop(ctx, dockerCli, backend, opts, args)
|
||||
return runStop(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -53,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
|
||||
func runStop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts stopOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,6 +65,10 @@ func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Stop(ctx, name, api.StopOptions{
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
|
||||
@@ -27,14 +27,15 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type topOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func topCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := topOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -42,7 +43,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
Use: "top [SERVICES...]",
|
||||
Short: "Display the running processes",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runTop(ctx, dockerCli, backend, opts, args)
|
||||
return runTop(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -54,11 +55,16 @@ type (
|
||||
topEntries map[string]string
|
||||
)
|
||||
|
||||
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
|
||||
func runTop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts topOptions, services []string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers, err := backend.Top(ctx, projectName, services)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -21,9 +21,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
var topTestCases = []struct {
|
||||
@@ -202,7 +203,7 @@ var topTestCases = []struct {
|
||||
}
|
||||
|
||||
// TestRunTopCore only tests the core functionality of runTop: formatting
|
||||
// and printing of the output of (api.Service).Top().
|
||||
// and printing of the output of (api.Compose).Top().
|
||||
func TestRunTopCore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -321,7 +322,7 @@ func TestRunTopCore(t *testing.T) {
|
||||
|
||||
func trim(s string) string {
|
||||
var out bytes.Buffer
|
||||
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(s), "\n") {
|
||||
out.WriteString(strings.TrimSpace(line))
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
@@ -31,10 +31,11 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v5/cmd/display"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
// composeOptions hold options common to `up` and `run` to run compose project
|
||||
@@ -109,7 +110,7 @@ func (opts upOptions) OnExit() api.Cascade {
|
||||
}
|
||||
}
|
||||
|
||||
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
up := upOptions{}
|
||||
create := createOptions{}
|
||||
build := buildOptions{ProjectOptions: p}
|
||||
@@ -140,7 +141,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
return fmt.Errorf("no service selected")
|
||||
}
|
||||
|
||||
return runUp(ctx, dockerCli, backend, create, up, build, project, services)
|
||||
return runUp(ctx, dockerCli, backendOptions, create, up, build, project, services)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -165,6 +166,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
|
||||
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers")
|
||||
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information")
|
||||
flags.BoolVar(&build.quiet, "quiet-build", false, "Suppress the build output")
|
||||
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
|
||||
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services")
|
||||
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services")
|
||||
@@ -186,6 +188,9 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
|
||||
//nolint:gocyclo
|
||||
func validateFlags(up *upOptions, create *createOptions) error {
|
||||
if up.waitTimeout < 0 {
|
||||
return fmt.Errorf("--wait-timeout must be a non-negative integer")
|
||||
}
|
||||
if up.exitCodeFrom != "" && !up.cascadeFail {
|
||||
up.cascadeStop = true
|
||||
}
|
||||
@@ -223,10 +228,11 @@ func validateFlags(up *upOptions, create *createOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func runUp(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
backend api.Service,
|
||||
backendOptions *BackendOptions,
|
||||
createOptions createOptions,
|
||||
upOptions upOptions,
|
||||
buildOptions buildOptions,
|
||||
@@ -260,7 +266,7 @@ func runUp(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bo.Services = services
|
||||
bo.Services = project.ServiceNames()
|
||||
bo.Deps = !upOptions.noDeps
|
||||
build = &bo
|
||||
}
|
||||
@@ -275,7 +281,15 @@ func runUp(
|
||||
Inherit: !createOptions.noInherit,
|
||||
Timeout: createOptions.GetTimeout(),
|
||||
QuietPull: createOptions.quietPull,
|
||||
AssumeYes: createOptions.AssumeYes,
|
||||
}
|
||||
|
||||
if createOptions.AssumeYes {
|
||||
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if upOptions.noStart {
|
||||
@@ -317,7 +331,10 @@ func runUp(
|
||||
attach = attachSet.Elements()
|
||||
}
|
||||
|
||||
timeout := time.Duration(upOptions.waitTimeout) * time.Second
|
||||
var timeout time.Duration
|
||||
if upOptions.waitTimeout > 0 {
|
||||
timeout = time.Duration(upOptions.waitTimeout) * time.Second
|
||||
}
|
||||
return backend.Up(ctx, project, api.UpOptions{
|
||||
Create: create,
|
||||
Start: api.StartOptions{
|
||||
@@ -330,7 +347,7 @@ func runUp(
|
||||
WaitTimeout: timeout,
|
||||
Watch: upOptions.watch,
|
||||
Services: services,
|
||||
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain",
|
||||
NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func TestApplyScaleOpt(t *testing.T) {
|
||||
@@ -48,3 +50,42 @@ func TestApplyScaleOpt(t *testing.T) {
|
||||
assert.Equal(t, *bar.Scale, 3)
|
||||
assert.Equal(t, *bar.Deploy.Replicas, 3)
|
||||
}
|
||||
|
||||
func TestUpOptions_OnExit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upOptions
|
||||
want api.Cascade
|
||||
}{
|
||||
{
|
||||
name: "no cascade",
|
||||
args: upOptions{},
|
||||
want: api.CascadeIgnore,
|
||||
},
|
||||
{
|
||||
name: "cascade stop",
|
||||
args: upOptions{cascadeStop: true},
|
||||
want: api.CascadeStop,
|
||||
},
|
||||
{
|
||||
name: "cascade fail",
|
||||
args: upOptions{cascadeFail: true},
|
||||
want: api.CascadeFail,
|
||||
},
|
||||
{
|
||||
name: "both set - stop takes precedence",
|
||||
args: upOptions{
|
||||
cascadeStop: true,
|
||||
cascadeFail: true,
|
||||
},
|
||||
want: api.CascadeStop,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.args.OnExit()
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/internal"
|
||||
)
|
||||
|
||||
type versionOptions struct {
|
||||
|
||||
@@ -21,10 +21,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/internal"
|
||||
"github.com/docker/compose/v5/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
|
||||
@@ -23,8 +23,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type vizOptions struct {
|
||||
@@ -35,7 +37,7 @@ type vizOptions struct {
|
||||
indentationStr string
|
||||
}
|
||||
|
||||
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := vizOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -51,7 +53,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
return err
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runViz(ctx, dockerCli, backend, &opts)
|
||||
return runViz(ctx, dockerCli, backendOptions, &opts)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -63,9 +65,15 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
|
||||
func runViz(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *vizOptions) error {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, nil)
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type volumesOptions struct {
|
||||
@@ -34,7 +36,7 @@ type volumesOptions struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
options := volumesOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -43,7 +45,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
Use: "volumes [OPTIONS] [SERVICE...]",
|
||||
Short: "List volumes",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runVol(ctx, dockerCli, backend, args, options)
|
||||
return runVol(ctx, dockerCli, backendOptions, args, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -54,25 +56,26 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
|
||||
project, _, err := options.projectOrName(ctx, dockerCli, services...)
|
||||
func runVol(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, options volumesOptions) error {
|
||||
project, name, err := options.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
names := project.ServiceNames()
|
||||
|
||||
if len(services) == 0 {
|
||||
services = names
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if !slices.Contains(names, service) {
|
||||
return fmt.Errorf("no such service: %s", service)
|
||||
if project != nil {
|
||||
names := project.ServiceNames()
|
||||
for _, service := range services {
|
||||
if !slices.Contains(names, service) {
|
||||
return fmt.Errorf("no such service: %s", service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumes, err := backend.Volumes(ctx, project, api.VolumesOptions{
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type waitOptions struct {
|
||||
@@ -34,7 +36,7 @@ type waitOptions struct {
|
||||
downProject bool
|
||||
}
|
||||
|
||||
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
opts := waitOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -47,7 +49,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: Adapt(func(ctx context.Context, services []string) error {
|
||||
opts.services = services
|
||||
statusCode, err = runWait(ctx, dockerCli, backend, &opts)
|
||||
statusCode, err = runWait(ctx, dockerCli, backendOptions, &opts)
|
||||
return err
|
||||
}),
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
@@ -60,12 +62,16 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
|
||||
func runWait(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *waitOptions) (int64, error) {
|
||||
_, name, err := opts.projectOrName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return backend.Wait(ctx, name, api.WaitOptions{
|
||||
Services: opts.services,
|
||||
DownProjectOnContainerExit: opts.downProject,
|
||||
|
||||
@@ -21,13 +21,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/internal/locker"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v5/cmd/formatter"
|
||||
"github.com/docker/compose/v5/internal/locker"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
type watchOptions struct {
|
||||
@@ -36,7 +37,7 @@ type watchOptions struct {
|
||||
noUp bool
|
||||
}
|
||||
|
||||
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
watchOpts := watchOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -53,7 +54,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
if cmd.Parent().Name() == "alpha" {
|
||||
logrus.Warn("watch command is now available as a top level command")
|
||||
}
|
||||
return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
|
||||
return runWatch(ctx, dockerCli, backendOptions, watchOpts, buildOpts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@@ -64,8 +65,13 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
|
||||
project, _, err := watchOpts.ToProject(ctx, dockerCli, services)
|
||||
func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
|
||||
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import (
|
||||
"github.com/morikuni/aec"
|
||||
@@ -14,18 +14,8 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
const (
|
||||
DRYRUN_PREFIX = " DRY-RUN MODE - "
|
||||
)
|
||||
|
||||
func TestNoopWriter(t *testing.T) {
|
||||
todo := context.TODO()
|
||||
writer := ContextWriter(todo)
|
||||
|
||||
assert.Equal(t, writer, &noopWriter{})
|
||||
}
|
||||
@@ -14,18 +14,25 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func JSON(out io.Writer) api.EventProcessor {
|
||||
return &jsonWriter{
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
type jsonWriter struct {
|
||||
out io.Writer
|
||||
done chan bool
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
@@ -34,29 +41,25 @@ type jsonMessage struct {
|
||||
Tail bool `json:"tail,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Current int64 `json:"current,omitempty"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
Percent int `json:"percent,omitempty"`
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Start(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-p.done:
|
||||
return nil
|
||||
}
|
||||
func (p *jsonWriter) Start(ctx context.Context, operation string) {
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Event(e Event) {
|
||||
func (p *jsonWriter) Event(e api.Resource) {
|
||||
message := &jsonMessage{
|
||||
DryRun: p.dryRun,
|
||||
Tail: false,
|
||||
ID: e.ID,
|
||||
Status: e.StatusText(),
|
||||
Text: e.Text,
|
||||
Status: e.StatusText,
|
||||
Details: e.Details,
|
||||
ParentID: e.ParentID,
|
||||
Current: e.Current,
|
||||
Total: e.Total,
|
||||
@@ -68,29 +71,11 @@ func (p *jsonWriter) Event(e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Events(events []Event) {
|
||||
func (p *jsonWriter) On(events ...api.Resource) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jsonWriter) TailMsgf(msg string, args ...interface{}) {
|
||||
message := &jsonMessage{
|
||||
DryRun: p.dryRun,
|
||||
Tail: true,
|
||||
ID: "",
|
||||
Text: fmt.Sprintf(msg, args...),
|
||||
Status: "",
|
||||
}
|
||||
marshal, err := json.Marshal(message)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(p.out, string(marshal))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Stop() {
|
||||
p.done <- true
|
||||
}
|
||||
|
||||
func (p *jsonWriter) HasMore(bool) {
|
||||
func (p *jsonWriter) Done(_ string, _ bool) {
|
||||
}
|
||||
62
cmd/display/json_test.go
Normal file
62
cmd/display/json_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2024 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 display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func TestJsonWriter_Event(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
w := &jsonWriter{
|
||||
out: &out,
|
||||
dryRun: true,
|
||||
}
|
||||
|
||||
event := api.Resource{
|
||||
ID: "service1",
|
||||
ParentID: "project",
|
||||
Status: api.Working,
|
||||
Text: api.StatusCreating,
|
||||
Current: 50,
|
||||
Total: 100,
|
||||
Percent: 50,
|
||||
}
|
||||
w.Event(event)
|
||||
|
||||
var actual jsonMessage
|
||||
err := json.Unmarshal(out.Bytes(), &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := jsonMessage{
|
||||
DryRun: true,
|
||||
ID: event.ID,
|
||||
ParentID: event.ParentID,
|
||||
Text: api.StatusCreating,
|
||||
Status: "Working",
|
||||
Current: event.Current,
|
||||
Total: event.Total,
|
||||
Percent: event.Percent,
|
||||
}
|
||||
assert.DeepEqual(t, expected, actual)
|
||||
}
|
||||
33
cmd/display/mode.go
Normal file
33
cmd/display/mode.go
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2024 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 display
|
||||
|
||||
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
|
||||
var Mode = ModeAuto
|
||||
|
||||
const (
|
||||
// ModeAuto detect console capabilities
|
||||
ModeAuto = "auto"
|
||||
// ModeTTY use terminal capability for advanced rendering
|
||||
ModeTTY = "tty"
|
||||
// ModePlain dump raw events to output
|
||||
ModePlain = "plain"
|
||||
// ModeQuiet don't display events
|
||||
ModeQuiet = "quiet"
|
||||
// ModeJSON outputs a machine-readable JSON stream
|
||||
ModeJSON = "json"
|
||||
)
|
||||
@@ -14,53 +14,43 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func Plain(out io.Writer) api.EventProcessor {
|
||||
return &plainWriter{
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
type plainWriter struct {
|
||||
out io.Writer
|
||||
done chan bool
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func (p *plainWriter) Start(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-p.done:
|
||||
return nil
|
||||
}
|
||||
func (p *plainWriter) Start(ctx context.Context, operation string) {
|
||||
}
|
||||
|
||||
func (p *plainWriter) Event(e Event) {
|
||||
func (p *plainWriter) Event(e api.Resource) {
|
||||
prefix := ""
|
||||
if p.dryRun {
|
||||
prefix = api.DRYRUN_PREFIX
|
||||
prefix = DRYRUN_PREFIX
|
||||
}
|
||||
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.StatusText)
|
||||
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
|
||||
}
|
||||
|
||||
func (p *plainWriter) Events(events []Event) {
|
||||
func (p *plainWriter) On(events ...api.Resource) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *plainWriter) TailMsgf(msg string, args ...interface{}) {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
if p.dryRun {
|
||||
msg = api.DRYRUN_PREFIX + msg
|
||||
}
|
||||
_, _ = fmt.Fprintln(p.out, msg)
|
||||
}
|
||||
|
||||
func (p *plainWriter) Stop() {
|
||||
p.done <- true
|
||||
func (p *plainWriter) Done(_ string, _ bool) {
|
||||
}
|
||||
@@ -14,24 +14,25 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func Quiet() api.EventProcessor {
|
||||
return &quiet{}
|
||||
}
|
||||
|
||||
type quiet struct{}
|
||||
|
||||
func (q quiet) Start(_ context.Context) error {
|
||||
return nil
|
||||
func (q *quiet) Start(_ context.Context, _ string) {
|
||||
}
|
||||
|
||||
func (q quiet) Stop() {
|
||||
func (q *quiet) Done(_ string, _ bool) {
|
||||
}
|
||||
|
||||
func (q quiet) Event(_ Event) {
|
||||
}
|
||||
|
||||
func (q quiet) Events(_ []Event) {
|
||||
}
|
||||
|
||||
func (q quiet) TailMsgf(_ string, _ ...interface{}) {
|
||||
func (q *quiet) On(_ ...api.Resource) {
|
||||
}
|
||||
@@ -14,14 +14,14 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package display
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type spinner struct {
|
||||
type Spinner struct {
|
||||
time time.Time
|
||||
index int
|
||||
chars []string
|
||||
@@ -29,7 +29,7 @@ type spinner struct {
|
||||
done string
|
||||
}
|
||||
|
||||
func newSpinner() *spinner {
|
||||
func NewSpinner() *Spinner {
|
||||
chars := []string{
|
||||
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func newSpinner() *spinner {
|
||||
done = "-"
|
||||
}
|
||||
|
||||
return &spinner{
|
||||
return &Spinner{
|
||||
index: 0,
|
||||
time: time.Now(),
|
||||
chars: chars,
|
||||
@@ -48,7 +48,7 @@ func newSpinner() *spinner {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *spinner) String() string {
|
||||
func (s *Spinner) String() string {
|
||||
if s.stop {
|
||||
return s.done
|
||||
}
|
||||
@@ -61,10 +61,10 @@ func (s *spinner) String() string {
|
||||
return s.chars[s.index]
|
||||
}
|
||||
|
||||
func (s *spinner) Stop() {
|
||||
func (s *Spinner) Stop() {
|
||||
s.stop = true
|
||||
}
|
||||
|
||||
func (s *spinner) Restart() {
|
||||
func (s *Spinner) Restart() {
|
||||
s.stop = false
|
||||
}
|
||||
664
cmd/display/tty.go
Normal file
664
cmd/display/tty.go
Normal file
@@ -0,0 +1,664 @@
|
||||
/*
|
||||
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 display
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/morikuni/aec"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
// Full creates an EventProcessor that render advanced UI within a terminal.
|
||||
// On Start, TUI lists task with a progress timer
|
||||
func Full(out io.Writer, info io.Writer, detached bool) api.EventProcessor {
|
||||
return &ttyWriter{
|
||||
out: out,
|
||||
info: info,
|
||||
tasks: map[string]*task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
detached: detached,
|
||||
}
|
||||
}
|
||||
|
||||
type ttyWriter struct {
|
||||
out io.Writer
|
||||
ids []string // tasks ids ordered as first event appeared
|
||||
tasks map[string]*task
|
||||
repeated bool
|
||||
numLines int
|
||||
done chan bool
|
||||
mtx *sync.Mutex
|
||||
dryRun bool // FIXME(ndeloof) (re)implement support for dry-run
|
||||
operation string
|
||||
ticker *time.Ticker
|
||||
suspended bool
|
||||
info io.Writer
|
||||
detached bool
|
||||
}
|
||||
|
||||
type task struct {
|
||||
ID string
|
||||
parent string // the resource this task receives updates from - other parents will be ignored
|
||||
parents utils.Set[string] // all resources to depend on this task
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
text string
|
||||
details string
|
||||
status api.EventStatus
|
||||
current int64
|
||||
percent int
|
||||
total int64
|
||||
spinner *Spinner
|
||||
}
|
||||
|
||||
func newTask(e api.Resource) task {
|
||||
t := task{
|
||||
ID: e.ID,
|
||||
parents: utils.NewSet[string](),
|
||||
startTime: time.Now(),
|
||||
text: e.Text,
|
||||
details: e.Details,
|
||||
status: e.Status,
|
||||
current: e.Current,
|
||||
percent: e.Percent,
|
||||
total: e.Total,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
if e.ParentID != "" {
|
||||
t.parent = e.ParentID
|
||||
t.parents.Add(e.ParentID)
|
||||
}
|
||||
if e.Status == api.Done || e.Status == api.Error {
|
||||
t.stop()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// update adjusts task state based on last received event
|
||||
func (t *task) update(e api.Resource) {
|
||||
if e.ParentID != "" {
|
||||
t.parents.Add(e.ParentID)
|
||||
// we may receive same event from distinct parents (typically: images sharing layers)
|
||||
// to avoid status to flicker, only accept updates from our first declared parent
|
||||
if t.parent != e.ParentID {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// update task based on received event
|
||||
switch e.Status {
|
||||
case api.Done, api.Error, api.Warning:
|
||||
if t.status != e.Status {
|
||||
t.stop()
|
||||
}
|
||||
case api.Working:
|
||||
t.hasMore()
|
||||
}
|
||||
t.status = e.Status
|
||||
t.text = e.Text
|
||||
t.details = e.Details
|
||||
// progress can only go up
|
||||
if e.Total > t.total {
|
||||
t.total = e.Total
|
||||
}
|
||||
if e.Current > t.current {
|
||||
t.current = e.Current
|
||||
}
|
||||
if e.Percent > t.percent {
|
||||
t.percent = e.Percent
|
||||
}
|
||||
}
|
||||
|
||||
func (t *task) stop() {
|
||||
t.endTime = time.Now()
|
||||
t.spinner.Stop()
|
||||
}
|
||||
|
||||
func (t *task) hasMore() {
|
||||
t.spinner.Restart()
|
||||
}
|
||||
|
||||
func (t *task) Completed() bool {
|
||||
switch t.status {
|
||||
case api.Done, api.Error, api.Warning:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Start(ctx context.Context, operation string) {
|
||||
w.ticker = time.NewTicker(100 * time.Millisecond)
|
||||
w.operation = operation
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// interrupted
|
||||
w.ticker.Stop()
|
||||
return
|
||||
case <-w.done:
|
||||
return
|
||||
case <-w.ticker.C:
|
||||
w.print()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Done(operation string, success bool) {
|
||||
w.print()
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
w.ticker.Stop()
|
||||
w.operation = ""
|
||||
w.done <- true
|
||||
}
|
||||
|
||||
func (w *ttyWriter) On(events ...api.Resource) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
for _, e := range events {
|
||||
if e.ID == "Compose" {
|
||||
_, _ = fmt.Fprintln(w.info, ErrorColor(e.Details))
|
||||
continue
|
||||
}
|
||||
|
||||
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) && !w.detached {
|
||||
// skip those events to avoid mix with container logs
|
||||
continue
|
||||
}
|
||||
w.event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) event(e api.Resource) {
|
||||
// Suspend print while a build is in progress, to avoid collision with buildkit Display
|
||||
if e.Text == api.StatusBuilding {
|
||||
w.ticker.Stop()
|
||||
w.suspended = true
|
||||
} else if w.suspended {
|
||||
w.ticker.Reset(100 * time.Millisecond)
|
||||
w.suspended = false
|
||||
}
|
||||
|
||||
if last, ok := w.tasks[e.ID]; ok {
|
||||
last.update(e)
|
||||
} else {
|
||||
t := newTask(e)
|
||||
w.tasks[e.ID] = &t
|
||||
w.ids = append(w.ids, e.ID)
|
||||
}
|
||||
w.printEvent(e)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) printEvent(e api.Resource) {
|
||||
if w.operation != "" {
|
||||
// event will be displayed by progress UI on ticker's ticks
|
||||
return
|
||||
}
|
||||
|
||||
var color colorFunc
|
||||
switch e.Status {
|
||||
case api.Working:
|
||||
color = SuccessColor
|
||||
case api.Done:
|
||||
color = SuccessColor
|
||||
case api.Warning:
|
||||
color = WarningColor
|
||||
case api.Error:
|
||||
color = ErrorColor
|
||||
}
|
||||
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) parentTasks() iter.Seq[*task] {
|
||||
return func(yield func(*task) bool) {
|
||||
for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
||||
t := w.tasks[id]
|
||||
if len(t.parents) == 0 {
|
||||
yield(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) childrenTasks(parent string) iter.Seq[*task] {
|
||||
return func(yield func(*task) bool) {
|
||||
for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
||||
t := w.tasks[id]
|
||||
if t.parents.Has(parent) {
|
||||
yield(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lineData holds pre-computed formatting for a task line
|
||||
type lineData struct {
|
||||
spinner string // rendered spinner with color
|
||||
prefix string // dry-run prefix if any
|
||||
taskID string // possibly abbreviated
|
||||
progress string // progress bar and size info
|
||||
status string // rendered status with color
|
||||
details string // possibly abbreviated
|
||||
timer string // rendered timer with color
|
||||
statusPad int // padding before status to align
|
||||
timerPad int // padding before timer to align
|
||||
statusColor colorFunc
|
||||
}
|
||||
|
||||
func (w *ttyWriter) print() {
|
||||
terminalWidth := goterm.Width()
|
||||
terminalHeight := goterm.Height()
|
||||
if terminalWidth <= 0 {
|
||||
terminalWidth = 80
|
||||
}
|
||||
if terminalHeight <= 0 {
|
||||
terminalHeight = 24
|
||||
}
|
||||
w.printWithDimensions(terminalWidth, terminalHeight)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) printWithDimensions(terminalWidth, terminalHeight int) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if len(w.tasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
up := w.numLines + 1
|
||||
if !w.repeated {
|
||||
up--
|
||||
w.repeated = true
|
||||
}
|
||||
b := aec.NewBuilder(
|
||||
aec.Hide, // Hide the cursor while we are printing
|
||||
aec.Up(uint(up)),
|
||||
aec.Column(0),
|
||||
)
|
||||
_, _ = fmt.Fprint(w.out, b.ANSI)
|
||||
defer func() {
|
||||
_, _ = fmt.Fprint(w.out, aec.Show)
|
||||
}()
|
||||
|
||||
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
|
||||
_, _ = fmt.Fprintln(w.out, firstLine)
|
||||
|
||||
// Collect parent tasks in original order
|
||||
allTasks := slices.Collect(w.parentTasks())
|
||||
|
||||
// Available lines: terminal height - 2 (header line + potential "more" line)
|
||||
maxLines := terminalHeight - 2
|
||||
if maxLines < 1 {
|
||||
maxLines = 1
|
||||
}
|
||||
|
||||
showMore := len(allTasks) > maxLines
|
||||
tasksToShow := allTasks
|
||||
if showMore {
|
||||
tasksToShow = allTasks[:maxLines-1] // Reserve one line for "more" message
|
||||
}
|
||||
|
||||
// collect line data and compute timerLen
|
||||
lines := make([]lineData, len(tasksToShow))
|
||||
var timerLen int
|
||||
for i, t := range tasksToShow {
|
||||
lines[i] = w.prepareLineData(t)
|
||||
if len(lines[i].timer) > timerLen {
|
||||
timerLen = len(lines[i].timer)
|
||||
}
|
||||
}
|
||||
|
||||
// shorten details/taskID to fit terminal width
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
// compute padding
|
||||
w.applyPadding(lines, terminalWidth, timerLen)
|
||||
|
||||
// Render lines
|
||||
numLines := 0
|
||||
for _, l := range lines {
|
||||
_, _ = fmt.Fprint(w.out, lineText(l))
|
||||
numLines++
|
||||
}
|
||||
|
||||
if showMore {
|
||||
moreCount := len(allTasks) - len(tasksToShow)
|
||||
moreText := fmt.Sprintf(" ... %d more", moreCount)
|
||||
pad := terminalWidth - len(moreText)
|
||||
if pad < 0 {
|
||||
pad = 0
|
||||
}
|
||||
_, _ = fmt.Fprintf(w.out, "%s%s\n", moreText, strings.Repeat(" ", pad))
|
||||
numLines++
|
||||
}
|
||||
|
||||
// Clear any remaining lines from previous render
|
||||
for i := numLines; i < w.numLines; i++ {
|
||||
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
|
||||
numLines++
|
||||
}
|
||||
w.numLines = numLines
|
||||
}
|
||||
|
||||
func (w *ttyWriter) applyPadding(lines []lineData, terminalWidth int, timerLen int) {
|
||||
var maxBeforeStatus int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
// Width before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
if beforeStatus > maxBeforeStatus {
|
||||
maxBeforeStatus = beforeStatus
|
||||
}
|
||||
}
|
||||
|
||||
for i, l := range lines {
|
||||
// Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
// statusPad aligns status; lineText adds 1 more space after statusPad
|
||||
l.statusPad = maxBeforeStatus - beforeStatus
|
||||
|
||||
// Format: beforeStatus + statusPad + space(1) + status
|
||||
lineLen := beforeStatus + l.statusPad + 1 + utf8.RuneCountInString(l.status)
|
||||
if l.details != "" {
|
||||
lineLen += 1 + utf8.RuneCountInString(l.details)
|
||||
}
|
||||
l.timerPad = terminalWidth - lineLen - timerLen
|
||||
if l.timerPad < 1 {
|
||||
l.timerPad = 1
|
||||
}
|
||||
lines[i] = l
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) adjustLineWidth(lines []lineData, timerLen int, terminalWidth int) {
|
||||
const minIDLen = 10
|
||||
maxStatusLen := maxStatusLength(lines)
|
||||
|
||||
// Iteratively truncate until all lines fit
|
||||
for range 100 { // safety limit
|
||||
maxBeforeStatus := maxBeforeStatusWidth(lines)
|
||||
overflow := computeOverflow(lines, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth)
|
||||
|
||||
if overflow <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// First try to truncate details, then taskID
|
||||
if !truncateDetails(lines, overflow) && !truncateLongestTaskID(lines, overflow, minIDLen) {
|
||||
break // Can't truncate further
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maxStatusLength returns the maximum status text length across all lines.
|
||||
func maxStatusLength(lines []lineData) int {
|
||||
var maxLen int
|
||||
for i := range lines {
|
||||
if len(lines[i].status) > maxLen {
|
||||
maxLen = len(lines[i].status)
|
||||
}
|
||||
}
|
||||
return maxLen
|
||||
}
|
||||
|
||||
// maxBeforeStatusWidth computes the maximum width before statusPad across all lines.
|
||||
// This is: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
func maxBeforeStatusWidth(lines []lineData) int {
|
||||
var maxWidth int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
width := 3 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
|
||||
if width > maxWidth {
|
||||
maxWidth = width
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
// computeOverflow calculates how many characters the widest line exceeds the terminal width.
|
||||
// Returns 0 or negative if all lines fit.
|
||||
func computeOverflow(lines []lineData, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth int) int {
|
||||
var maxOverflow int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
detailsLen := len(l.details)
|
||||
if detailsLen > 0 {
|
||||
detailsLen++ // space before details
|
||||
}
|
||||
// Line width: maxBeforeStatus + space(1) + status + details + minTimerPad(1) + timer
|
||||
lineWidth := maxBeforeStatus + 1 + maxStatusLen + detailsLen + 1 + timerLen
|
||||
overflow := lineWidth - terminalWidth
|
||||
if overflow > maxOverflow {
|
||||
maxOverflow = overflow
|
||||
}
|
||||
}
|
||||
return maxOverflow
|
||||
}
|
||||
|
||||
// truncateDetails tries to truncate the first line's details to reduce overflow.
|
||||
// Returns true if any truncation was performed.
|
||||
func truncateDetails(lines []lineData, overflow int) bool {
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
if len(l.details) > 3 {
|
||||
reduction := overflow
|
||||
if reduction > len(l.details)-3 {
|
||||
reduction = len(l.details) - 3
|
||||
}
|
||||
l.details = l.details[:len(l.details)-reduction-3] + "..."
|
||||
return true
|
||||
} else if l.details != "" {
|
||||
l.details = ""
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// truncateLongestTaskID truncates the longest taskID to reduce overflow.
|
||||
// Returns true if truncation was performed.
|
||||
func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
|
||||
longestIdx := -1
|
||||
longestLen := minIDLen
|
||||
for i := range lines {
|
||||
if len(lines[i].taskID) > longestLen {
|
||||
longestLen = len(lines[i].taskID)
|
||||
longestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if longestIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
l := &lines[longestIdx]
|
||||
reduction := overflow + 3 // account for "..."
|
||||
newLen := len(l.taskID) - reduction
|
||||
if newLen < minIDLen-3 {
|
||||
newLen = minIDLen - 3
|
||||
}
|
||||
if newLen > 0 {
|
||||
l.taskID = l.taskID[:newLen] + "..."
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *ttyWriter) prepareLineData(t *task) lineData {
|
||||
endTime := time.Now()
|
||||
if t.status != api.Working {
|
||||
endTime = t.startTime
|
||||
if (t.endTime != time.Time{}) {
|
||||
endTime = t.endTime
|
||||
}
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if w.dryRun {
|
||||
prefix = PrefixColor(DRYRUN_PREFIX)
|
||||
}
|
||||
|
||||
elapsed := endTime.Sub(t.startTime).Seconds()
|
||||
|
||||
var (
|
||||
hideDetails bool
|
||||
total int64
|
||||
current int64
|
||||
completion []string
|
||||
)
|
||||
|
||||
// only show the aggregated progress while the root operation is in-progress
|
||||
if t.status == api.Working {
|
||||
for child := range w.childrenTasks(t.ID) {
|
||||
if child.status == api.Working && child.total == 0 {
|
||||
hideDetails = true
|
||||
}
|
||||
total += child.total
|
||||
current += child.current
|
||||
r := len(percentChars) - 1
|
||||
p := child.percent
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
completion = append(completion, percentChars[r*p/100])
|
||||
}
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
hideDetails = true
|
||||
}
|
||||
|
||||
var progress string
|
||||
if len(completion) > 0 {
|
||||
progress = " [" + SuccessColor(strings.Join(completion, "")) + "]"
|
||||
if !hideDetails {
|
||||
progress += fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
|
||||
}
|
||||
}
|
||||
|
||||
return lineData{
|
||||
spinner: spinner(t),
|
||||
prefix: prefix,
|
||||
taskID: t.ID,
|
||||
progress: progress,
|
||||
status: t.text,
|
||||
statusColor: colorFn(t.status),
|
||||
details: t.details,
|
||||
timer: fmt.Sprintf("%.1fs", elapsed),
|
||||
}
|
||||
}
|
||||
|
||||
func lineText(l lineData) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.spinner)
|
||||
sb.WriteString(l.prefix)
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.taskID)
|
||||
sb.WriteString(l.progress)
|
||||
sb.WriteString(strings.Repeat(" ", l.statusPad))
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.statusColor(l.status))
|
||||
if l.details != "" {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.details)
|
||||
}
|
||||
sb.WriteString(strings.Repeat(" ", l.timerPad))
|
||||
sb.WriteString(TimerColor(l.timer))
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var (
|
||||
spinnerDone = "✔"
|
||||
spinnerWarning = "!"
|
||||
spinnerError = "✘"
|
||||
)
|
||||
|
||||
func spinner(t *task) string {
|
||||
switch t.status {
|
||||
case api.Done:
|
||||
return SuccessColor(spinnerDone)
|
||||
case api.Warning:
|
||||
return WarningColor(spinnerWarning)
|
||||
case api.Error:
|
||||
return ErrorColor(spinnerError)
|
||||
default:
|
||||
return CountColor(t.spinner.String())
|
||||
}
|
||||
}
|
||||
|
||||
func colorFn(s api.EventStatus) colorFunc {
|
||||
switch s {
|
||||
case api.Done:
|
||||
return SuccessColor
|
||||
case api.Warning:
|
||||
return WarningColor
|
||||
case api.Error:
|
||||
return ErrorColor
|
||||
default:
|
||||
return nocolor
|
||||
}
|
||||
}
|
||||
|
||||
func numDone(tasks map[string]*task) int {
|
||||
i := 0
|
||||
for _, t := range tasks {
|
||||
if t.status != api.Working {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// lenAnsi count of user-perceived characters in ANSI string.
|
||||
func lenAnsi(s string) int {
|
||||
length := 0
|
||||
ansiCode := false
|
||||
for _, r := range s {
|
||||
if r == '\x1b' {
|
||||
ansiCode = true
|
||||
continue
|
||||
}
|
||||
if ansiCode && r == 'm' {
|
||||
ansiCode = false
|
||||
continue
|
||||
}
|
||||
if !ansiCode {
|
||||
length++
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
var percentChars = strings.Split("⠀⡀⣀⣄⣤⣦⣶⣷⣿", "")
|
||||
424
cmd/display/tty_test.go
Normal file
424
cmd/display/tty_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
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 display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func newTestWriter() (*ttyWriter, *bytes.Buffer) {
|
||||
var buf bytes.Buffer
|
||||
w := &ttyWriter{
|
||||
out: &buf,
|
||||
info: &buf,
|
||||
tasks: map[string]*task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
operation: "pull",
|
||||
}
|
||||
return w, &buf
|
||||
}
|
||||
|
||||
func addTask(w *ttyWriter, id, text, details string, status api.EventStatus) {
|
||||
t := &task{
|
||||
ID: id,
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: text,
|
||||
details: details,
|
||||
status: status,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[id] = t
|
||||
w.ids = append(w.ids, id)
|
||||
}
|
||||
|
||||
// extractLines parses the output buffer and returns lines without ANSI control sequences
|
||||
func extractLines(buf *bytes.Buffer) []string {
|
||||
content := buf.String()
|
||||
// Split by newline
|
||||
rawLines := strings.Split(content, "\n")
|
||||
var lines []string
|
||||
for _, line := range rawLines {
|
||||
// Skip empty lines and lines that are just ANSI codes
|
||||
if lenAnsi(line) > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_LinesFitTerminalWidth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
taskID string
|
||||
status string
|
||||
details string
|
||||
terminalWidth int
|
||||
}{
|
||||
{
|
||||
name: "short task fits wide terminal",
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "layer abc123",
|
||||
terminalWidth: 100,
|
||||
},
|
||||
{
|
||||
name: "long details truncated to fit",
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "downloading layer sha256:abc123def456789xyz0123456789abcdef",
|
||||
terminalWidth: 50,
|
||||
},
|
||||
{
|
||||
name: "long taskID truncated to fit",
|
||||
taskID: "very-long-image-name-that-exceeds-terminal-width",
|
||||
status: "Pulling",
|
||||
details: "",
|
||||
terminalWidth: 40,
|
||||
},
|
||||
{
|
||||
name: "both long taskID and details",
|
||||
taskID: "my-very-long-service-name-here",
|
||||
status: "Downloading",
|
||||
details: "layer sha256:abc123def456789xyz0123456789",
|
||||
terminalWidth: 50,
|
||||
},
|
||||
{
|
||||
name: "narrow terminal",
|
||||
taskID: "service-name",
|
||||
status: "Pulling",
|
||||
details: "some details",
|
||||
terminalWidth: 35,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
addTask(w, tc.taskID, tc.status, tc.details, api.Working)
|
||||
|
||||
w.printWithDimensions(tc.terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= tc.terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, tc.terminalWidth, line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_MultipleTasksFitTerminalWidth(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Add multiple tasks with varying lengths
|
||||
addTask(w, "Image nginx", "Pulling", "layer sha256:abc123", api.Working)
|
||||
addTask(w, "Image postgres-database", "Pulling", "downloading", api.Working)
|
||||
addTask(w, "Image redis", "Pulled", "", api.Done)
|
||||
|
||||
terminalWidth := 60
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_VeryNarrowTerminal(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
addTask(w, "Image nginx", "Pulling", "details", api.Working)
|
||||
|
||||
terminalWidth := 30
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_TaskWithProgress(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Create parent task
|
||||
parent := &task{
|
||||
ID: "Image nginx",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: "Pulling",
|
||||
status: api.Working,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks["Image nginx"] = parent
|
||||
w.ids = append(w.ids, "Image nginx")
|
||||
|
||||
// Create child tasks to trigger progress display
|
||||
for i := 0; i < 3; i++ {
|
||||
child := &task{
|
||||
ID: "layer" + string(rune('a'+i)),
|
||||
parents: map[string]struct{}{"Image nginx": {}},
|
||||
startTime: time.Now(),
|
||||
text: "Downloading",
|
||||
status: api.Working,
|
||||
total: 1000,
|
||||
current: 500,
|
||||
percent: 50,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[child.ID] = child
|
||||
w.ids = append(w.ids, child.ID)
|
||||
}
|
||||
|
||||
terminalWidth := 80
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_DetailsCorrectlyTruncated(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "downloading layer sha256:abc123def456789xyz",
|
||||
},
|
||||
}
|
||||
|
||||
terminalWidth := 50
|
||||
timerLen := 5
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
// Verify the line fits
|
||||
detailsLen := len(lines[0].details)
|
||||
if detailsLen > 0 {
|
||||
detailsLen++ // space before details
|
||||
}
|
||||
// widthWithoutDetails = 5 + prefix(0) + taskID(9) + progress(0) + status(7) + timer(5) = 26
|
||||
lineWidth := 5 + len(lines[0].taskID) + len(lines[0].status) + detailsLen + timerLen
|
||||
|
||||
assert.Assert(t, lineWidth <= terminalWidth,
|
||||
"line width %d should not exceed terminal width %d (taskID=%q, details=%q)",
|
||||
lineWidth, terminalWidth, lines[0].taskID, lines[0].details)
|
||||
|
||||
// Verify details were truncated (not removed entirely)
|
||||
assert.Assert(t, lines[0].details != "", "details should be truncated, not removed")
|
||||
assert.Assert(t, strings.HasSuffix(lines[0].details, "..."), "truncated details should end with ...")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_TaskIDCorrectlyTruncated(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "very-long-image-name-that-exceeds-minimum-length",
|
||||
status: "Pulling",
|
||||
details: "",
|
||||
},
|
||||
}
|
||||
|
||||
terminalWidth := 40
|
||||
timerLen := 5
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
lineWidth := 5 + len(lines[0].taskID) + 7 + timerLen
|
||||
|
||||
assert.Assert(t, lineWidth <= terminalWidth,
|
||||
"line width %d should not exceed terminal width %d (taskID=%q)",
|
||||
lineWidth, terminalWidth, lines[0].taskID)
|
||||
|
||||
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."), "truncated taskID should end with ...")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_NoTruncationNeeded(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
originalDetails := "short"
|
||||
originalTaskID := "Image foo"
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: originalTaskID,
|
||||
status: "Pulling",
|
||||
details: originalDetails,
|
||||
},
|
||||
}
|
||||
|
||||
// Wide terminal, nothing should be truncated
|
||||
w.adjustLineWidth(lines, 5, 100)
|
||||
|
||||
assert.Equal(t, originalTaskID, lines[0].taskID, "taskID should not be modified")
|
||||
assert.Equal(t, originalDetails, lines[0].details, "details should not be modified")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_DetailsRemovedWhenTooShort(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "abc", // Very short, can't be meaningfully truncated
|
||||
},
|
||||
}
|
||||
|
||||
// Terminal so narrow that even minimal details + "..." wouldn't help
|
||||
w.adjustLineWidth(lines, 5, 28)
|
||||
|
||||
assert.Equal(t, "", lines[0].details, "details should be removed entirely when too short to truncate")
|
||||
}
|
||||
|
||||
// stripAnsi removes ANSI escape codes from a string
|
||||
func stripAnsi(s string) string {
|
||||
var result strings.Builder
|
||||
inAnsi := false
|
||||
for _, r := range s {
|
||||
if r == '\x1b' {
|
||||
inAnsi = true
|
||||
continue
|
||||
}
|
||||
if inAnsi {
|
||||
// ANSI sequences end with a letter (m, h, l, G, etc.)
|
||||
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||
inAnsi = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_PulledAndPullingWithLongIDs(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Add a completed task with long ID
|
||||
completedTask := &task{
|
||||
ID: "Image docker.io/library/nginx-long-name",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now().Add(-2 * time.Second),
|
||||
endTime: time.Now(),
|
||||
text: "Pulled",
|
||||
status: api.Done,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
completedTask.spinner.Stop()
|
||||
w.tasks[completedTask.ID] = completedTask
|
||||
w.ids = append(w.ids, completedTask.ID)
|
||||
|
||||
// Add a pending task with long ID
|
||||
pendingTask := &task{
|
||||
ID: "Image docker.io/library/postgres-database",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: "Pulling",
|
||||
status: api.Working,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[pendingTask.ID] = pendingTask
|
||||
w.ids = append(w.ids, pendingTask.ID)
|
||||
|
||||
terminalWidth := 50
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
// Strip all ANSI codes from output and split by newline
|
||||
stripped := stripAnsi(buf.String())
|
||||
lines := strings.Split(stripped, "\n")
|
||||
|
||||
// Filter non-empty lines
|
||||
var nonEmptyLines []string
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
nonEmptyLines = append(nonEmptyLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Expected output format (50 runes per task line)
|
||||
expected := `[+] pull 1/2
|
||||
✔ Image docker.io/library/nginx-l... Pulled 2.0s
|
||||
⠋ Image docker.io/library/postgre... Pulling 0.0s`
|
||||
|
||||
expectedLines := strings.Split(expected, "\n")
|
||||
|
||||
// Debug output
|
||||
t.Logf("Actual output:\n")
|
||||
for i, line := range nonEmptyLines {
|
||||
t.Logf(" line %d (%2d runes): %q", i, utf8.RuneCountInString(line), line)
|
||||
}
|
||||
|
||||
// Verify number of lines
|
||||
assert.Equal(t, len(expectedLines), len(nonEmptyLines), "number of lines should match")
|
||||
|
||||
// Verify each line matches expected
|
||||
for i, line := range nonEmptyLines {
|
||||
if i < len(expectedLines) {
|
||||
assert.Equal(t, expectedLines[i], line,
|
||||
"line %d should match expected", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify task lines fit within terminal width (strict - no tolerance)
|
||||
for i, line := range nonEmptyLines {
|
||||
if i > 0 { // Skip header line
|
||||
runeCount := utf8.RuneCountInString(line)
|
||||
assert.Assert(t, runeCount <= terminalWidth,
|
||||
"line %d has %d runes which exceeds terminal width %d: %q",
|
||||
i, runeCount, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLenAnsi(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"hello", 5},
|
||||
{"\x1b[32mhello\x1b[0m", 5},
|
||||
{"\x1b[1;32mgreen\x1b[0m text", 10},
|
||||
{"", 0},
|
||||
{"\x1b[0m", 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := lenAnsi(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,81 +20,73 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
"github.com/morikuni/aec"
|
||||
)
|
||||
|
||||
var disableAnsi bool
|
||||
|
||||
func ansi(code string) string {
|
||||
return fmt.Sprintf("\033%s", code)
|
||||
}
|
||||
|
||||
func SaveCursor() {
|
||||
func saveCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("7"))
|
||||
// see https://github.com/morikuni/aec/pull/5
|
||||
fmt.Print(aec.Save)
|
||||
}
|
||||
|
||||
func RestoreCursor() {
|
||||
func restoreCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("8"))
|
||||
// see https://github.com/morikuni/aec/pull/5
|
||||
fmt.Print(aec.Restore)
|
||||
}
|
||||
|
||||
func HideCursor() {
|
||||
func showCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("[?25l"))
|
||||
fmt.Print(aec.Show)
|
||||
}
|
||||
|
||||
func ShowCursor() {
|
||||
func moveCursor(y, x int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("[?25h"))
|
||||
fmt.Print(aec.Position(uint(y), uint(x)))
|
||||
}
|
||||
|
||||
func MoveCursor(y, x int) {
|
||||
func carriageReturn() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
|
||||
fmt.Print(aec.Column(0))
|
||||
}
|
||||
|
||||
func MoveCursorX(pos int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
|
||||
}
|
||||
|
||||
func ClearLine() {
|
||||
func clearLine() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not move cursor from its current position
|
||||
fmt.Print(ansi("[2K"))
|
||||
fmt.Print(aec.EraseLine(aec.EraseModes.Tail))
|
||||
}
|
||||
|
||||
func MoveCursorUp(lines int) {
|
||||
func moveCursorUp(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not add new lines
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
|
||||
fmt.Print(aec.Up(uint(lines)))
|
||||
}
|
||||
|
||||
func MoveCursorDown(lines int) {
|
||||
func moveCursorDown(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not add new lines
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
|
||||
fmt.Print(aec.Down(uint(lines)))
|
||||
}
|
||||
|
||||
func NewLine() {
|
||||
func newLine() {
|
||||
// Like \n
|
||||
fmt.Print("\012")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package formatter
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -58,6 +59,9 @@ const (
|
||||
Auto = "auto"
|
||||
)
|
||||
|
||||
// ansiColorOffset is the offset for basic foreground colors in ANSI escape codes.
|
||||
const ansiColorOffset = 30
|
||||
|
||||
// SetANSIMode configure formatter for colored output on ANSI-compliant console
|
||||
func SetANSIMode(streams command.Streams, ansi string) {
|
||||
if !useAnsi(streams, ansi) {
|
||||
@@ -91,11 +95,15 @@ func ansiColor(code, s string, formatOpts ...string) string {
|
||||
|
||||
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
|
||||
func ansiColorCode(code string, formatOpts ...string) string {
|
||||
res := "\033["
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\033[")
|
||||
for _, c := range formatOpts {
|
||||
res = fmt.Sprintf("%s%s;", res, c)
|
||||
sb.WriteString(c)
|
||||
sb.WriteString(";")
|
||||
}
|
||||
return fmt.Sprintf("%s%sm", res, code)
|
||||
sb.WriteString(code)
|
||||
sb.WriteString("m")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func makeColorFunc(code string) colorFunc {
|
||||
@@ -122,8 +130,8 @@ func rainbowColor() colorFunc {
|
||||
func init() {
|
||||
colors := map[string]colorFunc{}
|
||||
for i, name := range names {
|
||||
colors[name] = makeColorFunc(strconv.Itoa(30 + i))
|
||||
colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1")
|
||||
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
|
||||
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
|
||||
}
|
||||
rainbow = []colorFunc{
|
||||
colors["cyan"],
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
// TemplateLegacyJSON the legacy json formatting value using go template
|
||||
TemplateLegacyJSON = "{{json.}}"
|
||||
// PRETTY is the constant for default formats on list commands
|
||||
//
|
||||
// Deprecated: use TABLE
|
||||
PRETTY = "pretty"
|
||||
// TABLE Print output in table format with column headers (default)
|
||||
|
||||
@@ -23,10 +23,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -104,7 +105,7 @@ type ContainerContext struct {
|
||||
// used in the template. It's currently only used to detect use of the .Size
|
||||
// field which (if used) automatically sets the '--size' option when making
|
||||
// the API call.
|
||||
FieldsUsed map[string]interface{}
|
||||
FieldsUsed map[string]any
|
||||
}
|
||||
|
||||
// NewContainerContext creates a new context for rendering containers
|
||||
@@ -273,7 +274,7 @@ func (c *ContainerContext) Networks() string {
|
||||
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
|
||||
func (c *ContainerContext) Size() string {
|
||||
if c.FieldsUsed == nil {
|
||||
c.FieldsUsed = map[string]interface{}{}
|
||||
c.FieldsUsed = map[string]any{}
|
||||
}
|
||||
c.FieldsUsed["Size"] = struct{}{}
|
||||
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
||||
|
||||
@@ -22,11 +22,11 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
// Print prints formatted lists in different formats
|
||||
func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
|
||||
func Print(toJSON any, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
|
||||
switch strings.ToLower(format) {
|
||||
case TABLE, PRETTY, "":
|
||||
return PrintPrettySection(outWriter, writerFn, headers...)
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestPrint(t *testing.T) {
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) {
|
||||
assert.NilError(t, Print(testList, TABLE, b, func(w io.Writer) {
|
||||
for _, t := range testList {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ import (
|
||||
const standardIndentation = " "
|
||||
|
||||
// ToStandardJSON return a string with the JSON representation of the interface{}
|
||||
func ToStandardJSON(i interface{}) (string, error) {
|
||||
func ToStandardJSON(i any) (string, error) {
|
||||
return ToJSON(i, "", standardIndentation)
|
||||
}
|
||||
|
||||
// ToJSON return a string with the JSON representation of the interface{}
|
||||
func ToJSON(i interface{}, prefix string, indentation string) (string, error) {
|
||||
func ToJSON(i any, prefix string, indentation string) (string, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
@@ -26,8 +26,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
// LogConsumer consume logs from services and format them
|
||||
@@ -56,10 +57,6 @@ func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Register(name string) {
|
||||
l.register(name)
|
||||
}
|
||||
|
||||
func (l *logConsumer) register(name string) *presenter {
|
||||
var p *presenter
|
||||
root, _, found := strings.Cut(name, " ")
|
||||
@@ -73,9 +70,12 @@ func (l *logConsumer) register(name string) *presenter {
|
||||
} else {
|
||||
cf := monochrome
|
||||
if l.color {
|
||||
if name == api.WatchLogger {
|
||||
switch name {
|
||||
case "":
|
||||
cf = monochrome
|
||||
case api.WatchLogger:
|
||||
cf = makeColorFunc("92")
|
||||
} else {
|
||||
default:
|
||||
cf = nextColor()
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (l *logConsumer) register(name string) *presenter {
|
||||
l.presenters.Store(name, p)
|
||||
l.computeWidth()
|
||||
if l.prefix {
|
||||
l.presenters.Range(func(key, value interface{}) bool {
|
||||
l.presenters.Range(func(key, value any) bool {
|
||||
p := value.(*presenter)
|
||||
p.setPrefix(l.width)
|
||||
return true
|
||||
@@ -120,9 +120,9 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
|
||||
}
|
||||
p := l.getPresenter(container)
|
||||
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
for line := range strings.SplitSeq(message, "\n") {
|
||||
if l.timestamp {
|
||||
_, _ = fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
|
||||
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (l *logConsumer) Status(container, msg string) {
|
||||
|
||||
func (l *logConsumer) computeWidth() {
|
||||
width := 0
|
||||
l.presenters.Range(func(key, value interface{}) bool {
|
||||
l.presenters.Range(func(key, value any) bool {
|
||||
p := value.(*presenter)
|
||||
if len(p.name) > width {
|
||||
width = len(p.name)
|
||||
@@ -184,7 +184,3 @@ func (l logDecorator) Status(container, msg string) {
|
||||
l.decorated.Status(container, msg)
|
||||
l.After()
|
||||
}
|
||||
|
||||
func (l logDecorator) Register(container string) {
|
||||
l.decorated.Register(container)
|
||||
}
|
||||
|
||||
@@ -22,15 +22,17 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
|
||||
"github.com/docker/compose/v5/internal/tracing"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
const DISPLAY_ERROR_TIME = 10
|
||||
@@ -48,8 +50,8 @@ func (ke *KeyboardError) printError(height int, info string) {
|
||||
if ke.shouldDisplay() {
|
||||
errMessage := ke.err.Error()
|
||||
|
||||
MoveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
||||
ClearLine()
|
||||
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
||||
clearLine()
|
||||
|
||||
fmt.Print(errMessage)
|
||||
}
|
||||
@@ -90,6 +92,7 @@ const (
|
||||
type LogKeyboard struct {
|
||||
kError KeyboardError
|
||||
Watch *KeyboardWatch
|
||||
Detach func()
|
||||
IsDockerDesktopActive bool
|
||||
logLevel KEYBOARD_LOG_LEVEL
|
||||
signalChannel chan<- os.Signal
|
||||
@@ -133,7 +136,7 @@ func (lk *LogKeyboard) createBuffer(lines int) {
|
||||
|
||||
if lines > 0 {
|
||||
allocateSpace(lines)
|
||||
MoveCursorUp(lines)
|
||||
moveCursorUp(lines)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,57 +149,51 @@ func (lk *LogKeyboard) printNavigationMenu() {
|
||||
height := goterm.Height()
|
||||
menu := lk.navigationMenu()
|
||||
|
||||
MoveCursorX(0)
|
||||
SaveCursor()
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
lk.kError.printError(height, menu)
|
||||
|
||||
MoveCursor(height-extraLines(menu), 0)
|
||||
ClearLine()
|
||||
moveCursor(height-extraLines(menu), 0)
|
||||
clearLine()
|
||||
fmt.Print(menu)
|
||||
|
||||
MoveCursorX(0)
|
||||
RestoreCursor()
|
||||
carriageReturn()
|
||||
restoreCursor()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) navigationMenu() string {
|
||||
var openDDInfo string
|
||||
var items []string
|
||||
if lk.IsDockerDesktopActive {
|
||||
openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
|
||||
items = append(items, shortcutKeyColor("v")+navColor(" View in Docker Desktop"))
|
||||
}
|
||||
|
||||
var openDDUI string
|
||||
if openDDInfo != "" {
|
||||
openDDUI = navColor(" ")
|
||||
}
|
||||
if lk.IsDockerDesktopActive {
|
||||
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
|
||||
items = append(items, shortcutKeyColor("o")+navColor(" View Config"))
|
||||
}
|
||||
|
||||
var watchInfo string
|
||||
if openDDInfo != "" || openDDUI != "" {
|
||||
watchInfo = navColor(" ")
|
||||
}
|
||||
isEnabled := " Enable"
|
||||
if lk.Watch != nil && lk.Watch.Watching {
|
||||
isEnabled = " Disable"
|
||||
}
|
||||
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
|
||||
return openDDInfo + openDDUI + watchInfo
|
||||
items = append(items, shortcutKeyColor("w")+navColor(isEnabled+" Watch"))
|
||||
items = append(items, shortcutKeyColor("d")+navColor(" Detach"))
|
||||
|
||||
return strings.Join(items, " ")
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) clearNavigationMenu() {
|
||||
height := goterm.Height()
|
||||
MoveCursorX(0)
|
||||
SaveCursor()
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
// ClearLine()
|
||||
// clearLine()
|
||||
for i := 0; i < height; i++ {
|
||||
MoveCursorDown(1)
|
||||
ClearLine()
|
||||
moveCursorDown(1)
|
||||
clearLine()
|
||||
}
|
||||
RestoreCursor()
|
||||
restoreCursor()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
||||
@@ -290,6 +287,9 @@ func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
|
||||
|
||||
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
|
||||
switch kRune := event.Rune; kRune {
|
||||
case 'd':
|
||||
lk.clearNavigationMenu()
|
||||
lk.Detach()
|
||||
case 'v':
|
||||
lk.openDockerDesktop(ctx, project)
|
||||
case 'w':
|
||||
@@ -316,13 +316,15 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
|
||||
case keyboard.KeyCtrlC:
|
||||
_ = keyboard.Close()
|
||||
lk.clearNavigationMenu()
|
||||
ShowCursor()
|
||||
showCursor()
|
||||
|
||||
lk.logLevel = NONE
|
||||
// will notify main thread to kill and will handle gracefully
|
||||
lk.signalChannel <- syscall.SIGINT
|
||||
case keyboard.KeyCtrlZ:
|
||||
handleCtrlZ()
|
||||
case keyboard.KeyEnter:
|
||||
NewLine()
|
||||
newLine()
|
||||
lk.printNavigationMenu()
|
||||
}
|
||||
}
|
||||
@@ -334,11 +336,15 @@ func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) EnableDetach(detach func()) {
|
||||
lk.Detach = detach
|
||||
}
|
||||
|
||||
func allocateSpace(lines int) {
|
||||
for i := 0; i < lines; i++ {
|
||||
ClearLine()
|
||||
NewLine()
|
||||
MoveCursorX(0)
|
||||
clearLine()
|
||||
newLine()
|
||||
carriageReturn()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
Copyright 2024 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.
|
||||
@@ -14,26 +16,10 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
import "syscall"
|
||||
|
||||
type noopWriter struct{}
|
||||
|
||||
func (p *noopWriter) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *noopWriter) Event(Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Events([]Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Stop() {
|
||||
func handleCtrlZ() {
|
||||
_ = syscall.Kill(0, syscall.SIGSTOP)
|
||||
}
|
||||
25
cmd/formatter/shortcut_windows.go
Normal file
25
cmd/formatter/shortcut_windows.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
Copyright 2024 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 formatter
|
||||
|
||||
// handleCtrlZ is a no-op on Windows as SIGSTOP is not supported
|
||||
func handleCtrlZ() {
|
||||
// Windows doesn't support SIGSTOP/SIGCONT signals
|
||||
// Ctrl+Z behavior is handled differently by the Windows terminal
|
||||
}
|
||||
94
cmd/main.go
94
cmd/main.go
@@ -20,75 +20,61 @@ import (
|
||||
"os"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/cmdtrace"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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/compose"
|
||||
"github.com/docker/compose/v5/cmd/cmdtrace"
|
||||
"github.com/docker/compose/v5/cmd/compatibility"
|
||||
commands "github.com/docker/compose/v5/cmd/compose"
|
||||
"github.com/docker/compose/v5/cmd/prompt"
|
||||
"github.com/docker/compose/v5/internal"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
|
||||
func pluginMain() {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
// TODO(milas): this cast is safe but we should not need to do this,
|
||||
// we should expose the concrete service type so that we do not need
|
||||
// to rely on the `api.Service` interface internally
|
||||
backend := compose.NewComposeService(dockerCli).(commands.Backend)
|
||||
cmd := commands.RootCommand(dockerCli, backend)
|
||||
originalPreRunE := cmd.PersistentPreRunE
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// initialize the dockerCli instance
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
// compose-specific initialization
|
||||
dockerCliPostInitialize(dockerCli)
|
||||
|
||||
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil {
|
||||
logrus.Debugf("failed to enable tracing: %v", err)
|
||||
plugin.Run(
|
||||
func(cli command.Cli) *cobra.Command {
|
||||
backendOptions := &commands.BackendOptions{
|
||||
Options: []compose.Option{
|
||||
compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
|
||||
},
|
||||
}
|
||||
|
||||
if originalPreRunE != nil {
|
||||
return originalPreRunE(cmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
cmd := commands.RootCommand(cli, backendOptions)
|
||||
originalPreRunE := cmd.PersistentPreRunE
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// initialize the cli instance
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdtrace.Setup(cmd, cli, os.Args[1:]); err != nil {
|
||||
logrus.Debugf("failed to enable tracing: %v", err)
|
||||
}
|
||||
|
||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: 1,
|
||||
Status: err.Error(),
|
||||
if originalPreRunE != nil {
|
||||
return originalPreRunE(cmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
return cmd
|
||||
},
|
||||
manager.Metadata{
|
||||
|
||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: 1,
|
||||
Status: err.Error(),
|
||||
}
|
||||
})
|
||||
return cmd
|
||||
},
|
||||
metadata.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
Vendor: "Docker Inc.",
|
||||
Version: internal.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// dockerCliPostInitialize performs Compose-specific configuration for the
|
||||
// command.Cli instance provided by the plugin.Run() initialization.
|
||||
//
|
||||
// NOTE: This must be called AFTER plugin.PersistentPreRunE.
|
||||
func dockerCliPostInitialize(dockerCli command.Cli) {
|
||||
// HACK(milas): remove once docker/cli#4574 is merged; for now,
|
||||
// set it in a rather roundabout way by grabbing the underlying
|
||||
// concrete client and manually invoking an option on it
|
||||
_ = dockerCli.Apply(func(cli *command.DockerCli) error {
|
||||
if mobyClient, ok := cli.Client().(*client.Client); ok {
|
||||
_ = client.WithUserAgent("compose/" + internal.Version)(mobyClient)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
command.WithUserAgent("compose/"+internal.Version),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -22,10 +22,11 @@ import (
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v2/pkg/prompt" -package=prompt . UI
|
||||
//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v5/pkg/prompt" -package=prompt . UI
|
||||
|
||||
// UI - prompt user input
|
||||
type UI interface {
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Container: github.com/docker/compose-cli/pkg/prompt (interfaces: UI)
|
||||
// Source: github.com/docker/compose-cli/pkg/prompt (interfaces: UI)
|
||||
|
||||
// Package prompt is a generated GoMock package.
|
||||
package prompt
|
||||
@@ -1,7 +1,7 @@
|
||||
# About
|
||||
|
||||
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
|
||||
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
|
||||
application needs, which can interact with other services by relying on network(s). Docker Compose is designed
|
||||
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
|
||||
many other runtimes, typically cloud services or services natively provided by host.
|
||||
|
||||
@@ -55,8 +55,8 @@ JSON messages MUST include a `type` and a `message` attribute.
|
||||
|
||||
`type` can be either:
|
||||
- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI
|
||||
- `error`: Let's the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
|
||||
- `setenv`: Let's the plugin tell Compose how dependent services can access the created resource. See next section for further details.
|
||||
- `error`: Lets the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
|
||||
- `setenv`: Lets the plugin tell Compose how dependent services can access the created resource. See next section for further details.
|
||||
- `debug`: Those messages could help debugging the provider, but are not rendered to the user by default. They are rendered when Compose is started with `--verbose` flag.
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
# docker compose
|
||||
|
||||
```text
|
||||
@@ -126,6 +127,57 @@ get the postgres image for the db service from anywhere by using the `-f` flag a
|
||||
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
|
||||
```
|
||||
|
||||
#### Using an OCI published artifact
|
||||
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
|
||||
This allows you to distribute and version your Compose configurations as OCI artifacts.
|
||||
|
||||
To use a Compose file from an OCI registry:
|
||||
|
||||
```console
|
||||
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
|
||||
```
|
||||
|
||||
You can also combine OCI artifacts with local files:
|
||||
|
||||
```console
|
||||
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
|
||||
```
|
||||
|
||||
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
|
||||
`docker compose publish` command.
|
||||
|
||||
#### Using a git repository
|
||||
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
|
||||
|
||||
Using HTTPS:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git up
|
||||
```
|
||||
|
||||
Using SSH:
|
||||
```console
|
||||
$ docker compose -f git@github.com:user/repo.git up
|
||||
```
|
||||
|
||||
You can specify a specific branch, tag, or commit:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git@main up
|
||||
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
|
||||
$ docker compose -f https://github.com/user/repo.git@abc123 up
|
||||
```
|
||||
|
||||
You can also specify a subdirectory within the repository:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
|
||||
```
|
||||
|
||||
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
|
||||
git resources with local files:
|
||||
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
|
||||
```
|
||||
|
||||
### Use `-p` to specify a project name
|
||||
|
||||
Each configuration has a project name. Compose sets the project name using
|
||||
|
||||
@@ -22,9 +22,11 @@ run `docker compose build` to rebuild it.
|
||||
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
|
||||
| `--no-cache` | `bool` | | Do not use cache when building the image |
|
||||
| `--print` | `bool` | | Print equivalent bake file |
|
||||
| `--provenance` | `string` | | Add a provenance attestation |
|
||||
| `--pull` | `bool` | | Always attempt to pull a newer version of the image |
|
||||
| `--push` | `bool` | | Push service images |
|
||||
| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT |
|
||||
| `-q`, `--quiet` | `bool` | | Suppress the build output |
|
||||
| `--sbom` | `string` | | Add a SBOM attestation |
|
||||
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
|
||||
| `--with-dependencies` | `bool` | | Also build dependencies (transitively) |
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ the canonical format.
|
||||
| `--hash` | `string` | | Print the service config hash, one per line. |
|
||||
| `--images` | `bool` | | Print the image names, one per line. |
|
||||
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
|
||||
| `--models` | `bool` | | Print the model names, one per line. |
|
||||
| `--networks` | `bool` | | Print the network names, one per line. |
|
||||
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
|
||||
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
|
||||
|
||||
@@ -23,10 +23,12 @@ The events that can be received using this can be seen [here](/reference/cli/doc
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:-------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--json` | `bool` | | Output events as a stream of json objects |
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:---------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--json` | `bool` | | Output events as a stream of json objects |
|
||||
| `--since` | `string` | | Show all events created since timestamp |
|
||||
| `--until` | `string` | | Stream events until this timestamp |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -6,6 +6,12 @@ This is the equivalent of `docker exec` targeting a Compose service.
|
||||
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
|
||||
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
|
||||
|
||||
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
|
||||
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
|
||||
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
|
||||
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
|
||||
a script.
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
@@ -14,7 +20,7 @@ you can use a command such as `docker compose exec web sh` to get an interactive
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `-e`, `--env` | `stringArray` | | Set environment variables |
|
||||
| `--index` | `int` | `0` | Index of the container if service has multiple replicas |
|
||||
| `-T`, `--no-TTY` | `bool` | `true` | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
|
||||
| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. |
|
||||
| `--privileged` | `bool` | | Give extended privileges to the process |
|
||||
| `-u`, `--user` | `string` | | Run the command as this user |
|
||||
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command |
|
||||
@@ -28,3 +34,9 @@ This is the equivalent of `docker exec` targeting a Compose service.
|
||||
|
||||
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
|
||||
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
|
||||
|
||||
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
|
||||
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
|
||||
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
|
||||
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
|
||||
a script.
|
||||
@@ -7,6 +7,7 @@ Publish compose application
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
|
||||
| `--app` | `bool` | | Published compose application (includes referenced images) |
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
|
||||
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
|
||||
|
||||
@@ -5,9 +5,11 @@ Starts existing containers for a service
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:-------|:--------|:--------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| Name | Type | Default | Description |
|
||||
|:-----------------|:-------|:--------|:---------------------------------------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. |
|
||||
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -44,6 +44,7 @@ If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the contai
|
||||
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
|
||||
| `--no-start` | `bool` | | Don't start the services after creating them |
|
||||
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
|
||||
| `--quiet-build` | `bool` | | Suppress the build output |
|
||||
| `--quiet-pull` | `bool` | | Pull without printing progress information |
|
||||
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
|
||||
| `-V`, `--renew-anon-volumes` | `bool` | | Recreate anonymous volumes instead of retrieving data from the previous containers |
|
||||
|
||||
@@ -139,6 +139,17 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: insecure-registry
|
||||
value_type: stringArray
|
||||
default_value: '[]'
|
||||
description: |
|
||||
Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images
|
||||
deprecated: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: no-ansi
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
@@ -290,6 +301,57 @@ examples: |-
|
||||
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
|
||||
```
|
||||
|
||||
#### Using an OCI published artifact
|
||||
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
|
||||
This allows you to distribute and version your Compose configurations as OCI artifacts.
|
||||
|
||||
To use a Compose file from an OCI registry:
|
||||
|
||||
```console
|
||||
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
|
||||
```
|
||||
|
||||
You can also combine OCI artifacts with local files:
|
||||
|
||||
```console
|
||||
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
|
||||
```
|
||||
|
||||
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
|
||||
`docker compose publish` command.
|
||||
|
||||
#### Using a git repository
|
||||
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
|
||||
|
||||
Using HTTPS:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git up
|
||||
```
|
||||
|
||||
Using SSH:
|
||||
```console
|
||||
$ docker compose -f git@github.com:user/repo.git up
|
||||
```
|
||||
|
||||
You can specify a specific branch, tag, or commit:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git@main up
|
||||
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
|
||||
$ docker compose -f https://github.com/user/repo.git@abc123 up
|
||||
```
|
||||
|
||||
You can also specify a subdirectory within the repository:
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
|
||||
```
|
||||
|
||||
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
|
||||
git resources with local files:
|
||||
|
||||
```console
|
||||
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
|
||||
```
|
||||
|
||||
### Use `-p` to specify a project name
|
||||
|
||||
Each configuration has a project name. Compose sets the project name using
|
||||
|
||||
@@ -5,6 +5,26 @@ usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
|
||||
pname: docker compose alpha
|
||||
plink: docker_compose_alpha.yaml
|
||||
options:
|
||||
- option: app
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Published compose application (includes referenced images)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: insecure-registry
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Use insecure registry
|
||||
deprecated: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: oci-version
|
||||
value_type: string
|
||||
description: |
|
||||
|
||||
@@ -125,6 +125,15 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: provenance
|
||||
value_type: string
|
||||
description: Add a provenance attestation
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: pull
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
@@ -149,7 +158,16 @@ options:
|
||||
shorthand: q
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Don't print anything to STDOUT
|
||||
description: Suppress the build output
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: sbom
|
||||
value_type: string
|
||||
description: Add a SBOM attestation
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
|
||||
@@ -56,6 +56,16 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: models
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Print the model names, one per line.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: networks
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
|
||||
@@ -34,6 +34,24 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: since
|
||||
value_type: string
|
||||
description: Show all events created since timestamp
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: until
|
||||
value_type: string
|
||||
description: Stream events until this timestamp
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
|
||||
@@ -5,6 +5,12 @@ long: |-
|
||||
|
||||
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
|
||||
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
|
||||
|
||||
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
|
||||
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
|
||||
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
|
||||
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
|
||||
a script.
|
||||
usage: docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...]
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
@@ -52,12 +58,12 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: no-TTY
|
||||
- option: no-tty
|
||||
shorthand: T
|
||||
value_type: bool
|
||||
default_value: "true"
|
||||
description: |
|
||||
Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.
|
||||
Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user