mirror of https://github.com/docker/compose.git
Compare commits
No commits in common. "0.5.0" and "main" have entirely different histories.
|
@ -0,0 +1 @@
|
|||
bin/
|
|
@ -0,0 +1,2 @@
|
|||
core.autocrlf false
|
||||
*.golden text eol=lf
|
|
@ -0,0 +1,2 @@
|
|||
# global rules
|
||||
* @docker/compose-maintainers
|
|
@ -0,0 +1,55 @@
|
|||
name: 🐞 Bug
|
||||
description: File a bug/issue
|
||||
title: "[BUG] <title>"
|
||||
labels: ['status/0-triage', 'kind/bug']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Briefly describe the problem you are having.
|
||||
|
||||
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
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Compose Version
|
||||
description: |
|
||||
Paste output of `docker compose version` and `docker-compose version`.
|
||||
render: Text
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Docker Environment
|
||||
description: Paste output of `docker info`.
|
||||
render: Text
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
|
@ -0,0 +1,11 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Docker Community Slack
|
||||
url: https://dockr.ly/slack
|
||||
about: 'Use the #docker-compose channel'
|
||||
- name: Docker Support Forums
|
||||
url: https://forums.docker.com/c/open-source-projects/compose/15
|
||||
about: 'Use the "Open Source Projects > Compose" category'
|
||||
- name: 'Ask on Stack Overflow'
|
||||
url: https://stackoverflow.com/questions/tagged/docker-compose
|
||||
about: 'Use the [docker-compose] tag when creating new questions'
|
|
@ -0,0 +1,13 @@
|
|||
name: Feature request
|
||||
description: Missing functionality? Come tell us about it!
|
||||
labels:
|
||||
- kind/feature
|
||||
- status/0-triage
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What is the feature you want to see?
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,6 @@
|
|||
**What I did**
|
||||
|
||||
**Related issue**
|
||||
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
|
||||
|
||||
**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
|
|
@ -0,0 +1,44 @@
|
|||
# Security Policy
|
||||
|
||||
The maintainers of Docker Compose take security seriously. If you discover
|
||||
a security issue, please bring it to their attention right away!
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately
|
||||
to [security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
|
||||
received.
|
||||
|
||||
## Review Process
|
||||
|
||||
After receiving the report, an initial triage and technical analysis is
|
||||
performed to confirm the report and determine its scope. We may request
|
||||
additional information in this stage of the process.
|
||||
|
||||
Once a reviewer has confirmed the relevance of the report, a draft security
|
||||
advisory will be created on GitHub. The draft advisory will be used to discuss
|
||||
the issue with maintainers, the reporter(s), and where applicable, other
|
||||
affected parties under embargo.
|
||||
|
||||
If the vulnerability is accepted, a timeline for developing a patch, public
|
||||
disclosure, and patch release will be determined. If there is an embargo period
|
||||
on public disclosure before the patch release, the reporter(s) are expected to
|
||||
participate in the discussion of the timeline and abide by agreed upon dates
|
||||
for public disclosure.
|
||||
|
||||
## Accreditation
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you,
|
||||
although we will keep your name confidential if you request it. We also like to
|
||||
send gifts - if you're into swag, make sure to let us know. We do not currently
|
||||
offer a paid security bounty program at this time.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
This project docs not provide long-term supported versions, and only the current
|
||||
release and `main` branch are actively maintained. Docker Compose v1, and the
|
||||
corresponding [v1 branch](https://github.com/docker/compose/tree/v1) reached
|
||||
EOL and are no longer supported.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
# docker + moby deps require coordination
|
||||
- dependency-name: "github.com/docker/buildx"
|
||||
# buildx is still 0.x
|
||||
update-types: ["version-update:semver-minor"]
|
||||
- dependency-name: "github.com/moby/buildkit"
|
||||
# buildkit is still 0.x
|
||||
update-types: [ "version-update:semver-minor" ]
|
||||
- dependency-name: "github.com/docker/cli"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "github.com/docker/docker"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "github.com/containerd/containerd"
|
||||
# containerd major/minor must be kept in sync with moby
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
# OTEL dependencies should be upgraded in sync with engine, cli, buildkit and buildx projects
|
||||
- dependency-name: "go.opentelemetry.io/*"
|
|
@ -0,0 +1,59 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- "kind/feature"
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: false
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: true
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
unmarkComment: >
|
||||
This issue has been automatically marked as not stale anymore due to the recent activity.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been automatically closed because it had not recent activity during the stale period.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
|
@ -0,0 +1,333 @@
|
|||
name: ci
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
description: 'To run with tmate enter "debug_enabled"'
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
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:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- lint
|
||||
- validate-go-mod
|
||||
- validate-headers
|
||||
- validate-docs
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Run
|
||||
run: |
|
||||
make ${{ matrix.target }}
|
||||
|
||||
binary:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
||||
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
|
||||
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
|
||||
-
|
||||
name: Rename provenance and sbom
|
||||
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
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: compose-${{ env.PLATFORM_PAIR }}
|
||||
path: ./bin/release
|
||||
if-no-files-found: error
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
targets: test
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=test
|
||||
*.cache-to=type=gha,scope=test
|
||||
-
|
||||
name: Gather coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-data-unit
|
||||
path: bin/coverage/unit/
|
||||
if-no-files-found: error
|
||||
-
|
||||
name: Unit Test Summary
|
||||
uses: test-summary/action@v2
|
||||
with:
|
||||
paths: bin/coverage/unit/report.xml
|
||||
if: always()
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mode:
|
||||
- plugin
|
||||
- standalone
|
||||
engine:
|
||||
- 26
|
||||
- 27
|
||||
- 28
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
mode=${{ matrix.mode }}
|
||||
engine=${{ matrix.engine }}
|
||||
echo "MODE_ENGINE_PAIR=${mode}-${engine}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker ${{ matrix.engine }}
|
||||
run: |
|
||||
sudo systemctl stop docker.service
|
||||
sudo apt-get purge docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin
|
||||
sudo apt-get install curl
|
||||
curl -fsSL https://test.docker.com -o get-docker.sh
|
||||
sudo sh ./get-docker.sh --version ${{ matrix.engine }}
|
||||
|
||||
- name: Check Docker Version
|
||||
run: docker --version
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Docker Model
|
||||
run: |
|
||||
sudo apt-get install docker-model-plugin
|
||||
docker model version
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: Build example provider
|
||||
run: make example-provider
|
||||
|
||||
- name: Build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
source: .
|
||||
targets: binary-with-coverage
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=binary-linux-amd64
|
||||
*.cache-from=type=gha,scope=binary-e2e-${{ matrix.mode }}
|
||||
*.cache-to=type=gha,scope=binary-e2e-${{ matrix.mode }},mode=max
|
||||
env:
|
||||
BUILD_TAGS: e2e
|
||||
|
||||
- name: Setup tmate session
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
||||
uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Test plugin mode
|
||||
if: ${{ matrix.mode == 'plugin' }}
|
||||
run: |
|
||||
rm -rf ./bin/coverage/e2e
|
||||
mkdir -p ./bin/coverage/e2e
|
||||
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
|
||||
|
||||
- name: Gather coverage data
|
||||
if: ${{ matrix.mode == 'plugin' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-data-e2e-${{ env.MODE_ENGINE_PAIR }}
|
||||
path: bin/coverage/e2e/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Test standalone mode
|
||||
if: ${{ matrix.mode == 'standalone' }}
|
||||
run: |
|
||||
rm -f /usr/local/bin/docker-compose
|
||||
cp bin/build/docker-compose /usr/local/bin
|
||||
make e2e-compose-standalone
|
||||
|
||||
- name: e2e Test Summary
|
||||
uses: test-summary/action@v2
|
||||
with:
|
||||
paths: /tmp/report/report.xml
|
||||
if: always()
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- e2e
|
||||
steps:
|
||||
# codecov won't process the report without the source code available
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
- name: Download unit test coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage-data-unit
|
||||
path: coverage/unit
|
||||
merge-multiple: true
|
||||
- name: Download E2E test coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: coverage-data-e2e-*
|
||||
path: coverage/e2e
|
||||
merge-multiple: true
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
|
||||
- name: Store coverage report in GitHub Actions
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: go-covdata-txt
|
||||
path: ./coverage.txt
|
||||
if-no-files-found: error
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: write # to create a release (ncipollo/release-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- binary
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
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: List artifacts
|
||||
run: |
|
||||
tree -nh ./bin/release
|
||||
-
|
||||
name: Check artifacts
|
||||
run: |
|
||||
find bin/release -type f -exec file -e ascii -- {} +
|
||||
-
|
||||
name: GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
|
||||
with:
|
||||
artifacts: ./bin/release/*
|
||||
generateReleaseNotes: true
|
||||
draft: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -0,0 +1,51 @@
|
|||
# this workflow runs the remote validate bake target from docker/docs
|
||||
# to check if yaml reference docs used in this repo are valid
|
||||
name: docs-upstream
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'v[0-9]*'
|
||||
paths:
|
||||
- '.github/workflows/docs-upstream.yml'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/docs-upstream.yml'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
docs-yaml:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Upload reference YAML docs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-yaml
|
||||
path: docs/reference
|
||||
retention-days: 1
|
||||
|
||||
validate:
|
||||
uses: docker/docs/.github/workflows/validate-upstream.yml@main
|
||||
needs:
|
||||
- docs-yaml
|
||||
with:
|
||||
module-name: docker/compose
|
|
@ -0,0 +1,163 @@
|
|||
name: merge
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
REPO_SLUG: "docker/compose-bin"
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [desktop-windows, desktop-macos, desktop-m1]
|
||||
# mode: [plugin, standalone]
|
||||
mode: [plugin]
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
check-latest: true
|
||||
|
||||
- name: List Docker resources on machine
|
||||
run: |
|
||||
docker ps --all
|
||||
docker volume ls
|
||||
docker network ls
|
||||
docker image ls
|
||||
- name: Remove Docker resources on machine
|
||||
continue-on-error: true
|
||||
run: |
|
||||
docker kill $(docker ps -q)
|
||||
docker rm -f $(docker ps -aq)
|
||||
docker volume rm -f $(docker volume ls -q)
|
||||
docker ps --all
|
||||
|
||||
- name: Unit tests
|
||||
run: make test
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
make
|
||||
- name: Check arch of go compose binary
|
||||
run: |
|
||||
file ./bin/build/docker-compose
|
||||
if: ${{ !contains(matrix.os, 'desktop-windows') }}
|
||||
-
|
||||
name: Test plugin mode
|
||||
if: ${{ matrix.mode == 'plugin' }}
|
||||
run: |
|
||||
make e2e-compose
|
||||
-
|
||||
name: Test standalone mode
|
||||
if: ${{ matrix.mode == 'standalone' }}
|
||||
run: |
|
||||
make e2e-compose-standalone
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
|
||||
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:
|
||||
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
|
||||
needs: bin-image
|
||||
steps:
|
||||
-
|
||||
name: Generate Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ vars.DOCKERDESKTOP_APP_ID }}
|
||||
private-key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
|
||||
owner: docker
|
||||
repositories: |
|
||||
${{ secrets.DOCKERDESKTOP_REPO }}
|
||||
-
|
||||
name: Trigger Docker Desktop e2e with edge version
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'docker',
|
||||
repo: '${{ secrets.DOCKERDESKTOP_REPO }}',
|
||||
workflow_id: 'compose-edge-integration.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"image-tag": "${{ needs.bin-image.outputs.digest }}"
|
||||
}
|
||||
})
|
|
@ -0,0 +1,63 @@
|
|||
name: Scorecards supply-chain security
|
||||
on:
|
||||
# Only the default branch is supported.
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '44 9 * * 4'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Used to receive a badge.
|
||||
id-token: write
|
||||
# read permissions to all the other objects
|
||||
actions: read
|
||||
attestations: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
statuses: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.4.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # tag=v2.4.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
|
||||
# Publish the results for public repositories to enable scorecard badges. For more details, see
|
||||
# https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories, `publish_results` will automatically be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # tag=v4.5.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@3096afedf9873361b2b2f65e1445b13272c83eb8 # tag=v2.20.00
|
||||
with:
|
||||
sarif_file: results.sarif
|
|
@ -0,0 +1,33 @@
|
|||
name: 'Close stale issues'
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
days-before-issue-stale: 150 # marks stale after 5 months
|
||||
days-before-issue-close: 30 # closes 1 month after being marked with no action
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "kind/feature,kind/enhancement"
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
*.egg-info
|
||||
*.pyc
|
||||
/build
|
||||
/dist
|
||||
/docs/_site
|
||||
/venv
|
||||
fig.spec
|
||||
bin/
|
||||
/.vscode/
|
||||
coverage.out
|
||||
covdatafiles/
|
||||
.DS_Store
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
version: "2"
|
||||
run:
|
||||
concurrency: 2
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- copyloopvar
|
||||
- depguard
|
||||
- errcheck
|
||||
- errorlint
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gomodguard
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
all:
|
||||
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
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- paramTypeCombine
|
||||
- unnamedResult
|
||||
- whyNoLint
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- opinionated
|
||||
- style
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
gomodguard:
|
||||
blocked:
|
||||
modules:
|
||||
- github.com/pkg/errors:
|
||||
recommendations:
|
||||
- errors
|
||||
- fmt
|
||||
versions:
|
||||
- github.com/distribution/distribution:
|
||||
reason: use distribution/reference
|
||||
- gotest.tools:
|
||||
version: < 3.0.0
|
||||
reason: deprecated, pre-modules version
|
||||
lll:
|
||||
line-length: 200
|
||||
revive:
|
||||
rules:
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
29
.travis.yml
29
.travis.yml
|
@ -1,29 +0,0 @@
|
|||
language: python
|
||||
python:
|
||||
- '2.6'
|
||||
- '2.7'
|
||||
env:
|
||||
global:
|
||||
- secure: exbot0LTV/0Wic6ElKCrOZmh2ZrieuGwEqfYKf5rVuwu1sLngYRihh+lBL/hTwc79NSu829pbwiWfsQZrXbk/yvaS7avGR0CLDoipyPxlYa2/rfs/o4OdTZqXv0LcFmmd54j5QBMpWU1S+CYOwNkwas57trrvIpPbzWjMtfYzOU=
|
||||
install:
|
||||
- pip install .
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements-dev.txt
|
||||
- sudo curl -L -o /usr/local/bin/orchard https://github.com/orchardup/go-orchard/releases/download/2.0.5/linux
|
||||
- sudo chmod +x /usr/local/bin/orchard
|
||||
before_script:
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID || true; fi'
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts create $TRAVIS_JOB_ID; fi'
|
||||
script:
|
||||
- nosetests tests/unit
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then script/travis-integration; fi'
|
||||
after_script:
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID; fi'
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: orchard
|
||||
password:
|
||||
secure: M8UMupCLSsB1hV00Zn6ra8Vg81SCFBpbcRsa0nUw9kgXn9hOCESWYVHTqQ1ksWZOa8z6WMaqYtoosPKXGJQNf0wF/kEVDsMUeaZWOF/PqDkx1EwQ1diVfwlbN4/k0iX+Se7SrZfiWnJiAqiIPqToQipvLlJohqf8WwfPcVvILVE=
|
||||
on:
|
||||
tags: true
|
||||
repo: orchardup/fig
|
|
@ -0,0 +1,92 @@
|
|||
|
||||
### Prerequisites
|
||||
|
||||
* Windows:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
|
||||
* make
|
||||
* macOS:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
|
||||
* make
|
||||
* Linux:
|
||||
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
|
||||
* make
|
||||
|
||||
### Building the CLI
|
||||
|
||||
Once you have the prerequisites installed, you can build the CLI using:
|
||||
|
||||
```console
|
||||
make
|
||||
```
|
||||
|
||||
This will output a `docker-compose` CLI plugin for your host machine in
|
||||
`./bin/build`.
|
||||
|
||||
You can statically cross compile the CLI for Windows, macOS, and Linux using the
|
||||
`cross` target.
|
||||
|
||||
### Unit tests
|
||||
|
||||
To run all of the unit tests, run:
|
||||
|
||||
```console
|
||||
make test
|
||||
```
|
||||
|
||||
If you need to update a golden file simply do `go test ./... -test.update-golden`.
|
||||
|
||||
### End-to-end tests
|
||||
To run e2e tests, the Compose CLI binary needs to be built. All the commands to run e2e tests propose a version
|
||||
with the prefix `build-and-e2e` to first build the CLI before executing tests.
|
||||
|
||||
Note that this requires a local Docker Engine to be running.
|
||||
|
||||
#### Whole end-to-end tests suite
|
||||
|
||||
To execute both CLI and standalone e2e tests, run :
|
||||
|
||||
```console
|
||||
make e2e
|
||||
```
|
||||
|
||||
Or if you need to build the CLI, run:
|
||||
```console
|
||||
make build-and-e2e
|
||||
```
|
||||
|
||||
#### Plugin end-to-end tests suite
|
||||
|
||||
To execute CLI plugin e2e tests, run :
|
||||
|
||||
```console
|
||||
make e2e-compose
|
||||
```
|
||||
|
||||
Or if you need to build the CLI, run:
|
||||
```console
|
||||
make build-and-e2e-compose
|
||||
```
|
||||
|
||||
#### Standalone end-to-end tests suite
|
||||
|
||||
To execute the standalone CLI e2e tests, run :
|
||||
|
||||
```console
|
||||
make e2e-compose-standalone
|
||||
```
|
||||
|
||||
Or if you need to build the CLI, run:
|
||||
|
||||
```console
|
||||
make build-and-e2e-compose-standalone
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
To create a new release:
|
||||
* Check that the CI is green on the main branch for the commit you want to release
|
||||
* Run the release GitHub Actions workflow with a tag of form vx.y.z following existing tags.
|
||||
|
||||
This will automatically create a new tag, release and make binaries for
|
||||
Windows, macOS, and Linux available for download on the
|
||||
[releases page](https://github.com/docker/compose/releases).
|
161
CHANGES.md
161
CHANGES.md
|
@ -1,161 +0,0 @@
|
|||
Change log
|
||||
==========
|
||||
|
||||
0.5.0 (2014-07-11)
|
||||
------------------
|
||||
|
||||
- Fig now starts links when you run `fig run` or `fig up`.
|
||||
|
||||
For example, if you have a `web` service which depends on a `db` service, `fig run web ...` will start the `db` service.
|
||||
|
||||
- Environment variables can now be resolved from the environment that Fig is running in. Just specify it as a blank variable in your `fig.yml` and, if set, it'll be resolved:
|
||||
```
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
SESSION_SECRET:
|
||||
```
|
||||
|
||||
- `volumes_from` is now supported in `fig.yml`. All of the volumes from the specified services and containers will be mounted:
|
||||
|
||||
```
|
||||
volumes_from:
|
||||
- service_name
|
||||
- container_name
|
||||
```
|
||||
|
||||
- The `net` and `workdir` options are now supported in `fig.yml`.
|
||||
- The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option.
|
||||
- TTY behaviour is far more robust, and resizes are supported correctly.
|
||||
- Load YAML files safely.
|
||||
|
||||
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca and @mozz100 for their help with this release!
|
||||
|
||||
|
||||
0.4.2 (2014-06-18)
|
||||
------------------
|
||||
|
||||
- Fix various encoding errors when using `fig run`, `fig up` and `fig build`.
|
||||
|
||||
0.4.1 (2014-05-08)
|
||||
------------------
|
||||
|
||||
- Add support for Docker 0.11.0. (Thanks @marksteve!)
|
||||
- Make project name configurable. (Thanks @jefmathiot!)
|
||||
- Return correct exit code from `fig run`.
|
||||
|
||||
0.4.0 (2014-04-29)
|
||||
------------------
|
||||
|
||||
- Support Docker 0.9 and 0.10
|
||||
- Display progress bars correctly when pulling images (no more ski slopes)
|
||||
- `fig up` now stops all services when any container exits
|
||||
- Added support for the `privileged` config option in fig.yml (thanks @kvz!)
|
||||
- Shortened and aligned log prefixes in `fig up` output
|
||||
- Only containers started with `fig run` link back to their own service
|
||||
- Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!)
|
||||
- Error message improvements
|
||||
|
||||
0.3.2 (2014-03-05)
|
||||
------------------
|
||||
|
||||
- Added an `--rm` option to `fig run`. (Thanks @marksteve!)
|
||||
- Added an `expose` option to `fig.yml`.
|
||||
|
||||
0.3.1 (2014-03-04)
|
||||
------------------
|
||||
|
||||
- Added contribution instructions. (Thanks @kvz!)
|
||||
- Fixed `fig rm` throwing an error.
|
||||
- Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
|
||||
|
||||
0.3.0 (2014-03-03)
|
||||
------------------
|
||||
|
||||
- We now ship binaries for OS X and Linux. No more having to install with Pip!
|
||||
- Add `-f` flag to specify alternate `fig.yml` files
|
||||
- Add support for custom link names
|
||||
- Fix a bug where recreating would sometimes hang
|
||||
- Update docker-py to support Docker 0.8.0.
|
||||
- Various documentation improvements
|
||||
- Various error message improvements
|
||||
|
||||
Thanks @marksteve, @Gazler and @teozkr!
|
||||
|
||||
0.2.2 (2014-02-17)
|
||||
------------------
|
||||
|
||||
- Resolve dependencies using Cormen/Tarjan topological sort
|
||||
- Fix `fig up` not printing log output
|
||||
- Stop containers in reverse order to starting
|
||||
- Fix scale command not binding ports
|
||||
|
||||
Thanks to @barnybug and @dustinlacewell for their work on this release.
|
||||
|
||||
0.2.1 (2014-02-04)
|
||||
------------------
|
||||
|
||||
- General improvements to error reporting (#77, #79)
|
||||
|
||||
0.2.0 (2014-01-31)
|
||||
------------------
|
||||
|
||||
- Link services to themselves so run commands can access the running service. (#67)
|
||||
- Much better documentation.
|
||||
- Make service dependency resolution more reliable. (#48)
|
||||
- Load Fig configurations with a `.yaml` extension. (#58)
|
||||
|
||||
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
|
||||
|
||||
0.1.4 (2014-01-27)
|
||||
------------------
|
||||
|
||||
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
|
||||
|
||||
0.1.3 (2014-01-23)
|
||||
------------------
|
||||
|
||||
- Fix ports sometimes being configured incorrectly. (#46)
|
||||
- Fix log output sometimes not displaying. (#47)
|
||||
|
||||
0.1.2 (2014-01-22)
|
||||
------------------
|
||||
|
||||
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
|
||||
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
|
||||
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
|
||||
|
||||
0.1.1 (2014-01-17)
|
||||
------------------
|
||||
|
||||
- Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell!
|
||||
|
||||
0.1.0 (2014-01-16)
|
||||
------------------
|
||||
|
||||
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
|
||||
- Add `fig scale` command (#9)
|
||||
- Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19)
|
||||
- Truncate long commands in `fig ps` (#18)
|
||||
- Fill out CLI help banners for commands (#15, #16)
|
||||
- Show a friendlier error when `fig.yml` is missing (#4)
|
||||
- Fix bug with `fig build` logging (#3)
|
||||
- Fix bug where builds would time out if a step took a long time without generating output (#6)
|
||||
- Fix bug where streaming container output over the Unix socket raised an error (#7)
|
||||
|
||||
Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt.
|
||||
|
||||
0.0.2 (2014-01-02)
|
||||
------------------
|
||||
|
||||
- Improve documentation
|
||||
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
|
||||
- Improve `fig up` behaviour
|
||||
- Add confirmation prompt to `fig rm`
|
||||
- Add `fig build` command
|
||||
|
||||
0.0.1 (2013-12-20)
|
||||
------------------
|
||||
|
||||
Initial release.
|
||||
|
||||
|
322
CONTRIBUTING.md
322
CONTRIBUTING.md
|
@ -1,40 +1,217 @@
|
|||
# Contributing to Fig
|
||||
# Contributing to Docker
|
||||
|
||||
## Development environment
|
||||
Want to hack on Docker? Awesome! We have a contributor's guide that explains
|
||||
[setting up a Docker development environment and the contribution
|
||||
process](https://docs.docker.com/contribute/).
|
||||
|
||||
If you're looking contribute to [Fig](http://orchardup.github.io/fig/)
|
||||
but you're new to the project or maybe even to Python, here are the steps
|
||||
that should get you started.
|
||||
This page contains information about reporting issues as well as some tips and
|
||||
guidelines useful to experienced open source contributors. Finally, make sure
|
||||
you read our [community guidelines](#docker-community-guidelines) before you
|
||||
start participating.
|
||||
|
||||
1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example.
|
||||
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
|
||||
1. Enter the local directory `cd fig`.
|
||||
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
|
||||
1. Works for you? Run the test suite via `./script/test` to verify it won't break other usecases.
|
||||
1. All good? Commit and push to GitHub, and submit a pull request.
|
||||
## Topics
|
||||
|
||||
## Running the test suite
|
||||
- [Contributing to Docker](#contributing-to-docker)
|
||||
- [Topics](#topics)
|
||||
- [Reporting security issues](#reporting-security-issues)
|
||||
- [Reporting other issues](#reporting-other-issues)
|
||||
- [Quick contribution tips and guidelines](#quick-contribution-tips-and-guidelines)
|
||||
- [Pull requests are always welcome](#pull-requests-are-always-welcome)
|
||||
- [Talking to other Docker users and contributors](#talking-to-other-docker-users-and-contributors)
|
||||
- [Conventions](#conventions)
|
||||
- [Merge approval](#merge-approval)
|
||||
- [Sign your work](#sign-your-work)
|
||||
- [How can I become a maintainer?](#how-can-i-become-a-maintainer)
|
||||
- [Docker community guidelines](#docker-community-guidelines)
|
||||
- [Coding Style](#coding-style)
|
||||
|
||||
$ script/test
|
||||
## Reporting security issues
|
||||
|
||||
## Building binaries
|
||||
The Docker maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
Linux:
|
||||
Please **DO NOT** file a public issue, instead, send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
$ script/build-linux
|
||||
Security reports are greatly appreciated and we will publicly thank you for them.
|
||||
We also like to send gifts—if you're into Docker swag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program but are not
|
||||
ruling it out in the future.
|
||||
|
||||
OS X:
|
||||
|
||||
$ script/build-osx
|
||||
## Reporting other issues
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
A great way to contribute to the project is to send a detailed report when you
|
||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
||||
and will thank you for it!
|
||||
|
||||
## Sign your work
|
||||
Check that [our issue database](https://github.com/docker/compose/labels/Docker%20Compose%20V2)
|
||||
doesn't already include that problem or suggestion before submitting an issue.
|
||||
If you find a match, you can use the "subscribe" button to get notified of
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help to resolve it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolve the issue, please leave a comment.
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the
|
||||
patch, which certifies that you wrote it or otherwise have the right to
|
||||
pass it on as an open-source patch. The rules are pretty simple: if you
|
||||
can certify the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
When reporting issues, always include:
|
||||
|
||||
* The output of `docker version`.
|
||||
* The output of `docker context show`.
|
||||
* The output of `docker info`.
|
||||
|
||||
Also, include the steps required to reproduce the problem if possible and
|
||||
applicable. This information will help us review and fix your issue faster.
|
||||
When sending lengthy log files, consider posting them as a gist
|
||||
(https://gist.github.com).
|
||||
Don't forget to remove sensitive data from your log files before posting (you
|
||||
can replace those parts with "REDACTED").
|
||||
|
||||
_Note:_
|
||||
Maintainers might request additional information to diagnose an issue,
|
||||
if initial reporter doesn't answer within a reasonable delay (a few weeks),
|
||||
issue will be closed.
|
||||
|
||||
## Quick contribution tips and guidelines
|
||||
|
||||
This section gives the experienced contributor some tips and guidelines.
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
||||
it? Do it! We will appreciate it. Any significant change, like adding a backend,
|
||||
should be documented as
|
||||
[a GitHub issue](https://github.com/docker/compose/issues)
|
||||
before anybody starts working on it.
|
||||
|
||||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged!
|
||||
|
||||
### Talking to other Docker users and contributors
|
||||
|
||||
<table class="tg">
|
||||
<col width="45%">
|
||||
<col width="65%">
|
||||
<tr>
|
||||
<td>Community Slack</td>
|
||||
<td>
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/community/" target="_blank">with this link</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Forums</td>
|
||||
<td>
|
||||
A public forum for users to discuss questions and explore current design patterns and
|
||||
best practices about Docker and related projects in the Docker Ecosystem. To participate,
|
||||
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Twitter</td>
|
||||
<td>
|
||||
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
|
||||
to get updates on our products. You can also tweet us questions or just
|
||||
share blogs or stories.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack Overflow</td>
|
||||
<td>
|
||||
Stack Overflow has over 17000 Docker questions listed. We regularly
|
||||
monitor <a href="https://stackoverflow.com/questions/tagged/docker" target="_blank">Docker questions</a>
|
||||
and so do many other knowledgeable Docker users.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Conventions
|
||||
|
||||
Fork the repository and make changes on your fork in a feature branch:
|
||||
|
||||
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
||||
the issue.
|
||||
- If it's a feature branch, create an enhancement issue to announce
|
||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
||||
issue.
|
||||
|
||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||
it! Take a look at existing tests for inspiration. Also, end-to-end tests are
|
||||
available. Run the full test suite, both unit tests and e2e tests on your
|
||||
branch before submitting a pull request. See [BUILDING.md](BUILDING.md) for
|
||||
instructions to build and run tests.
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plug-ins that do this automatically.
|
||||
|
||||
Pull request descriptions should be as clear as possible and include a reference
|
||||
to all the issues that they address.
|
||||
|
||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
a comment after pushing. New commits show up in the pull request automatically,
|
||||
but the reviewers are notified only when you comment.
|
||||
|
||||
Pull requests must be cleanly rebased on top of the base branch without multiple branches
|
||||
mixed into the PR.
|
||||
|
||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
||||
feature branch to update your pull request rather than `merge master`.
|
||||
|
||||
Before you make a pull request, squash your commits into logical units of work
|
||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
||||
set of patches that should be reviewed together: for example, upgrading the
|
||||
version of a vendored dependency and taking advantage of its now available new
|
||||
feature constitute two separate units of work. Implementing a new function and
|
||||
calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
After every commit, make sure the test suite passes. Include documentation
|
||||
changes in the same pull request so that a revert would remove all traces of
|
||||
the feature or fix.
|
||||
|
||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull
|
||||
request description that closes an issue. Including references automatically
|
||||
closes the issue on a merge.
|
||||
|
||||
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
from the Git history.
|
||||
|
||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
### Merge approval
|
||||
|
||||
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
||||
indicate acceptance.
|
||||
|
||||
A change requires at least 2 LGTMs from the maintainers of each
|
||||
component affected.
|
||||
|
||||
For more details, see the [MAINTAINERS](MAINTAINERS) page.
|
||||
|
||||
### Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||
the below (from [developercertificate.org](https://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
|
@ -61,15 +238,102 @@ can certify the below (from [developercertificate.org](http://developercertifica
|
|||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
then you just add a line saying
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Random J Developer <random@developer.example.org>
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
using your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### How can I become a maintainer?
|
||||
|
||||
$ git commit --signoff
|
||||
The procedures for adding new maintainers are explained in the global
|
||||
[MAINTAINERS](https://github.com/docker/opensource/blob/main/MAINTAINERS)
|
||||
file in the
|
||||
[https://github.com/docker/opensource/](https://github.com/docker/opensource/)
|
||||
repository.
|
||||
|
||||
Don't forget: being a maintainer is a time investment. Make sure you
|
||||
will have time to make yourself available. You don't have to be a
|
||||
maintainer to make a difference on the project!
|
||||
|
||||
## Docker community guidelines
|
||||
|
||||
We want to keep the Docker community awesome, growing and collaborative. We need
|
||||
your help to keep it that way. To help with this we've come up with some general
|
||||
guidelines for the community as a whole:
|
||||
|
||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
||||
no regional, racial, gender or other abuse will be tolerated. We like
|
||||
nice people way better than mean ones!
|
||||
|
||||
* Encourage diversity and participation: Make everyone in our community feel
|
||||
welcome, regardless of their background and the extent of their
|
||||
contributions, and do everything possible to encourage participation in
|
||||
our community.
|
||||
|
||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
||||
you own, do not share private or sensitive information, and don't break
|
||||
the law.
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending it to a large number of people. Please
|
||||
consider this before you update. Also, remember that nobody likes spam.
|
||||
|
||||
* Don't send emails to the maintainers: There's no need to send emails to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
pull request. Instead of sending an email, GitHub mentions should be
|
||||
used to ping maintainers to review a pull request, a proposal or an
|
||||
issue.
|
||||
|
||||
## Coding Style
|
||||
|
||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
||||
community. While some of these standards may seem arbitrary, they somehow seem
|
||||
to result in a solid, consistent codebase.
|
||||
|
||||
It is possible that the code base does not currently comply with these
|
||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
||||
goes against the spirit of the guidelines. All new contributors should make their
|
||||
best effort to clean up and make the code base better than they left it.
|
||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
||||
code base easier for humans to navigate and understand. Always keep that in
|
||||
mind when nudging others to comply.
|
||||
|
||||
The rules:
|
||||
|
||||
1. All code should be formatted with `gofmt -s`.
|
||||
2. All code should pass the default levels of
|
||||
[`golint`](https://github.com/golang/lint).
|
||||
3. All code should follow the guidelines covered in [Effective
|
||||
Go](https://go.dev/doc/effective_go) and [Go Code Review
|
||||
Comments](https://go.dev/wiki/CodeReviewComments).
|
||||
4. Include code comments. Tell us the why, the history and the context.
|
||||
5. Document _all_ declarations and methods, even private ones. Declare
|
||||
expectations, caveats and anything else that may be important. If a type
|
||||
gets exported, having the comments already there will ensure it's ready.
|
||||
6. Variable name length should be proportional to its context and no longer.
|
||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
||||
In practice, short methods will have short variable names and globals will
|
||||
have longer names.
|
||||
7. No underscores in package names. If you need a compound name, step back,
|
||||
and re-examine why you need a compound name. If you still think you need a
|
||||
compound name, lose the underscore.
|
||||
8. No utils or helpers packages. If a function is not general enough to
|
||||
warrant its own package, it has not been written generally enough to be a
|
||||
part of a util package. Just leave it unexported and well-documented.
|
||||
9. All tests should run with `go test` and outside tooling should not be
|
||||
required. No, we don't need another unit testing framework. Assertion
|
||||
packages are acceptable if they provide _real_ incremental value.
|
||||
10. Even though we call these "rules" above, they are actually just
|
||||
guidelines. Since you've read all the rules, you now know that.
|
||||
|
||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||
reading through [Effective Go](https://go.dev/doc/effective_go). The
|
||||
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
|
||||
kool-aid is a lot easier than going thirsty.
|
||||
|
|
208
Dockerfile
208
Dockerfile
|
@ -1,11 +1,197 @@
|
|||
FROM orchardup/python:2.7
|
||||
ADD requirements.txt /code/
|
||||
WORKDIR /code/
|
||||
RUN pip install -r requirements.txt
|
||||
ADD requirements-dev.txt /code/
|
||||
RUN pip install -r requirements-dev.txt
|
||||
ADD . /code/
|
||||
RUN python setup.py develop
|
||||
RUN useradd -d /home/user -m -s /bin/bash user
|
||||
RUN chown -R user /code/
|
||||
USER user
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.23.10
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOLANGCI_LINT_VERSION=v2.0.2
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e"
|
||||
ARG DOCS_FORMATS="md,yaml"
|
||||
ARG LICENSE_FILES=".*\(Dockerfile\|Makefile\|\.go\|\.hcl\|\.sh\)"
|
||||
|
||||
# xx is a helper for cross-compilation
|
||||
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 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
|
||||
COPY --from=xx / /
|
||||
RUN apk add --no-cache \
|
||||
clang \
|
||||
docker \
|
||||
file \
|
||||
findutils \
|
||||
git \
|
||||
make \
|
||||
protoc \
|
||||
protobuf-dev
|
||||
WORKDIR /src
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
FROM base AS build-base
|
||||
COPY go.* .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go mod download
|
||||
|
||||
FROM build-base AS vendored
|
||||
RUN --mount=type=bind,target=.,rw \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go mod tidy && mkdir /out && cp go.mod go.sum /out
|
||||
|
||||
FROM scratch AS vendor-update
|
||||
COPY --from=vendored /out /
|
||||
|
||||
FROM vendored AS vendor-validate
|
||||
RUN --mount=type=bind,target=.,rw <<EOT
|
||||
set -e
|
||||
git add -A
|
||||
cp -rf /out/* .
|
||||
diff=$(git status --porcelain -- go.mod go.sum)
|
||||
if [ -n "$diff" ]; then
|
||||
echo >&2 'ERROR: Vendor result differs. Please vendor your package with "make go-mod-tidy"'
|
||||
echo "$diff"
|
||||
exit 1
|
||||
fi
|
||||
EOT
|
||||
|
||||
FROM build-base AS build
|
||||
ARG BUILD_TAGS
|
||||
ARG BUILD_FLAGS
|
||||
ARG TARGETPLATFORM
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--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 && \
|
||||
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
|
||||
xx-verify --static /out/docker-compose
|
||||
|
||||
FROM build-base AS lint
|
||||
ARG BUILD_TAGS
|
||||
ENV GOLANGCI_LINT_CACHE=/cache/golangci-lint
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/cache/golangci-lint \
|
||||
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
|
||||
golangci-lint cache status && \
|
||||
golangci-lint run --build-tags "$BUILD_TAGS" ./...
|
||||
|
||||
FROM build-base AS test
|
||||
ARG CGO_ENABLED=0
|
||||
ARG BUILD_TAGS
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
rm -rf /tmp/coverage && \
|
||||
mkdir -p /tmp/coverage && \
|
||||
rm -rf /tmp/report && \
|
||||
mkdir -p /tmp/report && \
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
|
||||
go tool covdata percent -i=/tmp/coverage
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test --link /tmp/coverage /
|
||||
COPY --from=test --link /tmp/report /
|
||||
|
||||
FROM base AS license-set
|
||||
ARG LICENSE_FILES
|
||||
RUN --mount=type=bind,target=.,rw \
|
||||
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
|
||||
find . -regex "${LICENSE_FILES}" | xargs addlicense -c 'Docker Compose CLI' -l apache && \
|
||||
mkdir /out && \
|
||||
find . -regex "${LICENSE_FILES}" | cpio -pdm /out
|
||||
|
||||
FROM scratch AS license-update
|
||||
COPY --from=set /out /
|
||||
|
||||
FROM base AS license-validate
|
||||
ARG LICENSE_FILES
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
|
||||
find . -regex "${LICENSE_FILES}" | xargs addlicense -check -c 'Docker Compose CLI' -l apache -ignore validate -ignore testdata -ignore resolvepath -v
|
||||
|
||||
FROM base AS docsgen
|
||||
WORKDIR /src
|
||||
RUN --mount=target=. \
|
||||
--mount=target=/root/.cache,type=cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go build -o /out/docsgen ./docs/yaml/main/generate.go
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} alpine AS docs-build
|
||||
RUN apk add --no-cache rsync git
|
||||
WORKDIR /src
|
||||
COPY --from=docsgen /out/docsgen /usr/bin
|
||||
ARG DOCS_FORMATS
|
||||
RUN --mount=target=/context \
|
||||
--mount=target=.,type=tmpfs <<EOT
|
||||
set -e
|
||||
rsync -a /context/. .
|
||||
docsgen --formats "$DOCS_FORMATS" --source "docs/reference"
|
||||
mkdir /out
|
||||
cp -r docs/reference /out
|
||||
EOT
|
||||
|
||||
FROM scratch AS docs-update
|
||||
COPY --from=docs-build /out /out
|
||||
|
||||
FROM docs-build AS docs-validate
|
||||
RUN --mount=target=/context \
|
||||
--mount=target=.,type=tmpfs <<EOT
|
||||
set -e
|
||||
rsync -a /context/. .
|
||||
git add -A
|
||||
rm -rf docs/reference/*
|
||||
cp -rf /out/* ./docs/
|
||||
if [ -n "$(git status --porcelain -- docs/reference)" ]; then
|
||||
echo >&2 'ERROR: Docs result differs. Please update with "make docs"'
|
||||
git status --porcelain -- docs/reference
|
||||
exit 1
|
||||
fi
|
||||
EOT
|
||||
|
||||
FROM scratch AS binary-unix
|
||||
COPY --link --from=build /out/docker-compose /
|
||||
FROM binary-unix AS binary-darwin
|
||||
FROM binary-unix AS binary-linux
|
||||
FROM scratch AS binary-windows
|
||||
COPY --link --from=build /out/docker-compose /docker-compose.exe
|
||||
FROM binary-$TARGETOS AS binary
|
||||
# enable scanning for this stage
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE=true
|
||||
|
||||
FROM --platform=$BUILDPLATFORM alpine AS releaser
|
||||
WORKDIR /work
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
RUN --mount=from=binary \
|
||||
mkdir -p /out && \
|
||||
# TODO: should just use standard arch
|
||||
TARGETARCH=$([ "$TARGETARCH" = "amd64" ] && echo "x86_64" || echo "$TARGETARCH"); \
|
||||
TARGETARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "$TARGETARCH"); \
|
||||
cp docker-compose* "/out/docker-compose-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}$(ls docker-compose* | sed -e 's/^docker-compose//')"
|
||||
|
||||
FROM scratch AS release
|
||||
COPY --from=releaser /out/ /
|
||||
|
|
13
LICENSE
13
LICENSE
|
@ -176,7 +176,18 @@
|
|||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2014 Orchard Laboratories Ltd.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
# 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"
|
10
MANIFEST.in
10
MANIFEST.in
|
@ -1,10 +0,0 @@
|
|||
include Dockerfile
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include requirements-dev.txt
|
||||
include tox.ini
|
||||
include *.md
|
||||
recursive-exclude tests *
|
||||
global-exclude *.pyc
|
||||
global-exclude *.pyo
|
||||
global-exclude *.un~
|
|
@ -0,0 +1,162 @@
|
|||
# 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.
|
||||
|
||||
PKG := github.com/docker/compose/v2
|
||||
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
||||
|
||||
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
|
||||
GO_BUILDTAGS ?= e2e
|
||||
DRIVE_PREFIX?=
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DETECTED_OS = Windows
|
||||
DRIVE_PREFIX=C:
|
||||
else
|
||||
DETECTED_OS = $(shell uname -s)
|
||||
endif
|
||||
|
||||
ifeq ($(DETECTED_OS),Windows)
|
||||
BINARY_EXT=.exe
|
||||
endif
|
||||
|
||||
BUILD_FLAGS?=
|
||||
TEST_FLAGS?=
|
||||
E2E_TEST?=
|
||||
ifneq ($(E2E_TEST),)
|
||||
TEST_FLAGS:=$(TEST_FLAGS) -run '$(E2E_TEST)'
|
||||
endif
|
||||
|
||||
EXCLUDE_E2E_TESTS?=
|
||||
ifneq ($(EXCLUDE_E2E_TESTS),)
|
||||
TEST_FLAGS:=$(TEST_FLAGS) -skip '$(EXCLUDE_E2E_TESTS)'
|
||||
endif
|
||||
|
||||
BUILDX_CMD ?= docker buildx
|
||||
|
||||
# DESTDIR overrides the output path for binaries and other artifacts
|
||||
# this is used by docker/docker-ce-packaging for the apt/rpm builds,
|
||||
# so it's important that the resulting binary ends up EXACTLY at the
|
||||
# path $DESTDIR/docker-compose when specified.
|
||||
#
|
||||
# See https://github.com/docker/docker-ce-packaging/blob/e43fbd37e48fde49d907b9195f23b13537521b94/rpm/SPECS/docker-compose-plugin.spec#L47
|
||||
#
|
||||
# By default, all artifacts go to subdirectories under ./bin/ in the
|
||||
# repo root, e.g. ./bin/build, ./bin/coverage, ./bin/release.
|
||||
DESTDIR ?=
|
||||
|
||||
all: build
|
||||
|
||||
.PHONY: build ## Build the compose cli-plugin
|
||||
build:
|
||||
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(or $(DESTDIR),./bin/build)/docker-compose$(BINARY_EXT)" ./cmd
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
$(BUILDX_CMD) bake binary
|
||||
|
||||
.PHONY: binary-with-coverage
|
||||
binary-with-coverage:
|
||||
$(BUILDX_CMD) bake binary-with-coverage
|
||||
|
||||
.PHONY: install
|
||||
install: binary
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
|
||||
|
||||
.PHONY: e2e-compose
|
||||
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e
|
||||
|
||||
.PHONY: e2e-compose-standalone
|
||||
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e
|
||||
|
||||
.PHONY: build-and-e2e-compose
|
||||
build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: build-and-e2e-compose-standalone
|
||||
build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: example-provider
|
||||
example-provider: ## build example provider for e2e tests
|
||||
go build -o bin/build/example-provider docs/examples/provider.go
|
||||
|
||||
.PHONY: mocks
|
||||
mocks:
|
||||
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0
|
||||
mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
|
||||
mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
|
||||
mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: e2e-compose e2e-compose-standalone ## Run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: build-and-e2e
|
||||
build-and-e2e: build e2e-compose e2e-compose-standalone ## Compile the compose cli-plugin and run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: cross
|
||||
cross: ## Compile the CLI for linux, darwin and windows
|
||||
$(BUILDX_CMD) bake binary-cross
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run unit tests
|
||||
$(BUILDX_CMD) bake test
|
||||
|
||||
.PHONY: cache-clear
|
||||
cache-clear: ## Clear the builder cache
|
||||
$(BUILDX_CMD) prune --force --filter type=exec.cachemount --filter=unused-for=24h
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## run linter(s)
|
||||
$(BUILDX_CMD) bake lint
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
gofumpt --version >/dev/null 2>&1 || go install mvdan.cc/gofumpt@latest
|
||||
gofumpt -w .
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## generate documentation
|
||||
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))
|
||||
$(BUILDX_CMD) bake --set "*.output=type=local,dest=$($@_TMP_OUT)" docs-update
|
||||
rm -rf ./docs/internal
|
||||
cp -R "$(DRIVE_PREFIX)$($@_TMP_OUT)"/out/* ./docs/
|
||||
rm -rf "$(DRIVE_PREFIX)$($@_TMP_OUT)"/*
|
||||
|
||||
.PHONY: validate-docs
|
||||
validate-docs: ## validate the doc does not change
|
||||
$(BUILDX_CMD) bake docs-validate
|
||||
|
||||
.PHONY: check-dependencies
|
||||
check-dependencies: ## check dependency updates
|
||||
go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all
|
||||
|
||||
.PHONY: validate-headers
|
||||
validate-headers: ## Check license header for all files
|
||||
$(BUILDX_CMD) bake license-validate
|
||||
|
||||
.PHONY: go-mod-tidy
|
||||
go-mod-tidy: ## Run go mod tidy in a container and output resulting go.mod and go.sum
|
||||
$(BUILDX_CMD) bake vendor-update
|
||||
|
||||
.PHONY: validate-go-mod
|
||||
validate-go-mod: ## Validate go.mod and go.sum are up-to-date
|
||||
$(BUILDX_CMD) bake vendor-validate
|
||||
|
||||
validate: validate-go-mod validate-headers validate-docs ## Validate sources
|
||||
|
||||
pre-commit: validate check-dependencies lint build test e2e-compose
|
||||
|
||||
help: ## Show help
|
||||
@echo Please specify a build target. The choices are:
|
||||
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
@ -0,0 +1,4 @@
|
|||
Docker Compose V2
|
||||
Copyright 2020 Docker Compose authors
|
||||
|
||||
This product includes software developed at Docker, Inc. (https://www.docker.com).
|
109
README.md
109
README.md
|
@ -1,49 +1,92 @@
|
|||
Fig
|
||||
===
|
||||
# Table of Contents
|
||||
- [Docker Compose v2](#docker-compose-v2)
|
||||
- [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
|
||||
|
||||
[](https://travis-ci.org/orchardup/fig)
|
||||
[](http://badge.fury.io/py/fig)
|
||||
[](https://github.com/docker/compose/releases/latest)
|
||||
[](https://pkg.go.dev/github.com/docker/compose/v2)
|
||||
[](https://github.com/docker/compose/actions?query=workflow%3Aci)
|
||||
[](https://goreportcard.com/report/github.com/docker/compose/v2)
|
||||
[](https://codecov.io/gh/docker/compose)
|
||||
[](https://api.securityscorecards.dev/projects/github.com/docker/compose)
|
||||

|
||||
|
||||
Fast, isolated development environments using Docker.
|
||||
Docker Compose is a tool for running multi-container applications on Docker
|
||||
defined using the [Compose file format](https://compose-spec.io).
|
||||
A Compose file is used to define how one or more containers that make up
|
||||
your application are configured.
|
||||
Once you have a Compose file, you can create and start your application with a
|
||||
single command: `docker compose up`.
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
> **Note**: About Docker Swarm
|
||||
> Docker Swarm used to rely on the legacy compose file format but did not adopted 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.
|
||||
|
||||
FROM orchardup/python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
# Where to get Docker Compose
|
||||
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
### Windows and macOS
|
||||
|
||||
Docker Compose is included in
|
||||
[Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
for Windows and macOS.
|
||||
|
||||
### Linux
|
||||
|
||||
You can download Docker Compose binaries from the
|
||||
[release page](https://github.com/docker/compose/releases) on this repository.
|
||||
|
||||
Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins`
|
||||
|
||||
Or copy it into one of these folders to install it system-wide:
|
||||
|
||||
* `/usr/local/lib/docker/cli-plugins` OR `/usr/local/libexec/docker/cli-plugins`
|
||||
* `/usr/lib/docker/cli-plugins` OR `/usr/libexec/docker/cli-plugins`
|
||||
|
||||
(might require making the downloaded file executable with `chmod +x`)
|
||||
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
Using Docker Compose is a three-step process:
|
||||
1. Define your app's environment with a `Dockerfile` so it can be
|
||||
reproduced anywhere.
|
||||
2. Define the services that make up your app in `compose.yaml` so
|
||||
they can be run together in an isolated environment.
|
||||
3. Lastly, run `docker compose up` and Compose will start and run your entire
|
||||
app.
|
||||
|
||||
A Compose file looks like this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "49100:22"
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/code
|
||||
redis:
|
||||
image: redis
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
Want to help develop Docker Compose? Check out our
|
||||
[contributing documentation](CONTRIBUTING.md).
|
||||
|
||||

|
||||
If you find an issue, please report it on the
|
||||
[issue tracker](https://github.com/docker/compose/issues/new/choose).
|
||||
|
||||
There are commands to:
|
||||
Legacy
|
||||
-------------
|
||||
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
|
||||
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
|
||||
|
||||
Installation and documentation
|
||||
------------------------------
|
||||
|
||||
Full documentation is available on [Fig's website](http://orchardup.github.io/fig/).
|
||||
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1).
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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 cmdtrace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// Setup should be called as part of the command's PersistentPreRunE
|
||||
// as soon as possible after initializing the dockerCli.
|
||||
//
|
||||
// It initializes the tracer for the CLI using both auto-detection
|
||||
// from the Docker context metadata as well as standard OTEL_ env
|
||||
// vars, creates a root span for the command, and wraps the actual
|
||||
// command invocation to ensure the span is properly finalized and
|
||||
// exported before exit.
|
||||
func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
|
||||
tracingShutdown, err := tracing.InitTracing(dockerCli)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing tracing: %w", err)
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
ctx, cmdSpan := otel.Tracer("").Start(
|
||||
ctx,
|
||||
"cli/"+strings.Join(commandName(cmd), "-"),
|
||||
)
|
||||
cmdSpan.SetAttributes(
|
||||
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
|
||||
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
|
||||
)
|
||||
|
||||
cmd.SetContext(ctx)
|
||||
wrapRunE(cmd, cmdSpan, tracingShutdown)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
|
||||
// method. This is necessary to capture the command result for reporting as well
|
||||
// as flushing any spans before exit.
|
||||
//
|
||||
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
|
||||
// only runs if RunE does _not_ return an error, but this should run unconditionally.
|
||||
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
|
||||
origRunE := c.RunE
|
||||
if origRunE == nil {
|
||||
origRun := c.Run
|
||||
//nolint:unparam // wrapper function for RunE, always returns nil by design
|
||||
origRunE = func(cmd *cobra.Command, args []string) error {
|
||||
origRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
c.Run = nil
|
||||
}
|
||||
|
||||
c.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
cmdErr := origRunE(cmd, args)
|
||||
if cmdSpan != nil {
|
||||
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
|
||||
// default exit code is 1 if a more descriptive error
|
||||
// wasn't returned
|
||||
exitCode := 1
|
||||
var statusErr dockercli.StatusError
|
||||
if errors.As(cmdErr, &statusErr) {
|
||||
exitCode = statusErr.StatusCode
|
||||
}
|
||||
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
|
||||
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
|
||||
attribute.Int("exit_code", exitCode),
|
||||
))
|
||||
|
||||
} else {
|
||||
cmdSpan.SetStatus(codes.Ok, "")
|
||||
}
|
||||
cmdSpan.End()
|
||||
}
|
||||
if tracingShutdown != nil {
|
||||
// use background for root context because the cmd's context might have
|
||||
// been canceled already
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
// TODO(milas): add an env var to enable logging from the
|
||||
// OTel components for debugging purposes
|
||||
_ = tracingShutdown(ctx)
|
||||
}
|
||||
return cmdErr
|
||||
}
|
||||
}
|
||||
|
||||
// commandName returns the path components for a given command,
|
||||
// in reverse alphabetical order for consistent usage metrics.
|
||||
//
|
||||
// The root Compose command and anything before (i.e. "docker")
|
||||
// are not included.
|
||||
//
|
||||
// For example:
|
||||
// - docker compose alpha watch -> [watch, alpha]
|
||||
// - docker-compose up -> [up]
|
||||
func commandName(cmd *cobra.Command) []string {
|
||||
var name []string
|
||||
for c := cmd; c != nil; c = c.Parent() {
|
||||
if c.Name() == commands.PluginName {
|
||||
break
|
||||
}
|
||||
name = append(name, c.Name())
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(name)))
|
||||
return name
|
||||
}
|
||||
|
||||
func getFlags(fs *flag.FlagSet) []string {
|
||||
var result []string
|
||||
fs.Visit(func(flag *flag.Flag) {
|
||||
result = append(result, flag.Name)
|
||||
})
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
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 cmdtrace
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func TestGetFlags(t *testing.T) {
|
||||
// Initialize flagSet with flags
|
||||
fs := flag.NewFlagSet("up", flag.ContinueOnError)
|
||||
var (
|
||||
detach string
|
||||
timeout string
|
||||
)
|
||||
fs.StringVar(&detach, "detach", "d", "")
|
||||
fs.StringVar(&timeout, "timeout", "t", "")
|
||||
_ = fs.Set("detach", "detach")
|
||||
_ = fs.Set("timeout", "timeout")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *flag.FlagSet
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "NoFlags",
|
||||
input: flag.NewFlagSet("NoFlags", flag.ContinueOnError),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Flags",
|
||||
input: fs,
|
||||
expected: []string{"detach", "timeout"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := getFlags(test.input)
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("Expected %v, but got %v", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupCmd func() *cobra.Command
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "docker compose alpha watch -> [watch, alpha]",
|
||||
setupCmd: func() *cobra.Command {
|
||||
dockerCmd := &cobra.Command{Use: "docker"}
|
||||
composeCmd := &cobra.Command{Use: commands.PluginName}
|
||||
alphaCmd := &cobra.Command{Use: "alpha"}
|
||||
watchCmd := &cobra.Command{Use: "watch"}
|
||||
|
||||
dockerCmd.AddCommand(composeCmd)
|
||||
composeCmd.AddCommand(alphaCmd)
|
||||
alphaCmd.AddCommand(watchCmd)
|
||||
|
||||
return watchCmd
|
||||
},
|
||||
want: []string{"watch", "alpha"},
|
||||
},
|
||||
{
|
||||
name: "docker-compose up -> [up]",
|
||||
setupCmd: func() *cobra.Command {
|
||||
dockerComposeCmd := &cobra.Command{Use: commands.PluginName}
|
||||
upCmd := &cobra.Command{Use: "up"}
|
||||
|
||||
dockerComposeCmd.AddCommand(upCmd)
|
||||
|
||||
return upCmd
|
||||
},
|
||||
want: []string{"up"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := tt.setupCmd()
|
||||
got := commandName(cmd)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("commandName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compatibility
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compose"
|
||||
)
|
||||
|
||||
func getCompletionCommands() []string {
|
||||
return []string{
|
||||
"__complete",
|
||||
"__completeNoDesc",
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolFlags() []string {
|
||||
return []string{
|
||||
"--debug", "-D",
|
||||
"--verbose",
|
||||
"--tls",
|
||||
"--tlsverify",
|
||||
}
|
||||
}
|
||||
|
||||
func getStringFlags() []string {
|
||||
return []string{
|
||||
"--tlscacert",
|
||||
"--tlscert",
|
||||
"--tlskey",
|
||||
"--host", "-H",
|
||||
"--context",
|
||||
"--log-level",
|
||||
}
|
||||
}
|
||||
|
||||
// Convert transforms standalone docker-compose args into CLI plugin compliant ones
|
||||
func Convert(args []string) []string {
|
||||
var rootFlags []string
|
||||
command := []string{compose.PluginName}
|
||||
l := len(args)
|
||||
ARGS:
|
||||
for i := 0; i < l; i++ {
|
||||
arg := args[i]
|
||||
if contains(getCompletionCommands(), arg) {
|
||||
command = append([]string{arg}, command...)
|
||||
continue
|
||||
}
|
||||
if arg != "" && arg[0] != '-' {
|
||||
command = append(command, args[i:]...)
|
||||
break
|
||||
}
|
||||
|
||||
switch arg {
|
||||
case "--verbose":
|
||||
arg = "--debug"
|
||||
case "-h":
|
||||
// docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it
|
||||
arg = "--help"
|
||||
case "--version", "-v":
|
||||
// redirect --version pseudo-command to actual command
|
||||
arg = "version"
|
||||
}
|
||||
|
||||
if contains(getBoolFlags(), arg) {
|
||||
rootFlags = append(rootFlags, arg)
|
||||
continue
|
||||
}
|
||||
for _, flag := range getStringFlags() {
|
||||
if arg == flag {
|
||||
i++
|
||||
if i >= l {
|
||||
fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
rootFlags = append(rootFlags, arg, args[i])
|
||||
continue ARGS
|
||||
}
|
||||
if strings.HasPrefix(arg, flag) {
|
||||
_, val, found := strings.Cut(arg, "=")
|
||||
if found {
|
||||
rootFlags = append(rootFlags, flag, val)
|
||||
continue ARGS
|
||||
}
|
||||
}
|
||||
}
|
||||
command = append(command, arg)
|
||||
}
|
||||
return append(rootFlags, command...)
|
||||
}
|
||||
|
||||
func contains(array []string, needle string) bool {
|
||||
for _, val := range array {
|
||||
if val == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compatibility
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func Test_convert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "compose only",
|
||||
args: []string{"up"},
|
||||
want: []string{"compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "with context",
|
||||
args: []string{"--context", "foo", "-f", "compose.yaml", "up"},
|
||||
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
|
||||
},
|
||||
{
|
||||
name: "with context arg",
|
||||
args: []string{"--context=foo", "-f", "compose.yaml", "up"},
|
||||
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
|
||||
},
|
||||
{
|
||||
name: "with host",
|
||||
args: []string{"--host", "tcp://1.2.3.4", "up"},
|
||||
want: []string{"--host", "tcp://1.2.3.4", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "compose --verbose",
|
||||
args: []string{"--verbose"},
|
||||
want: []string{"--debug", "compose"},
|
||||
},
|
||||
{
|
||||
name: "compose --version",
|
||||
args: []string{"--version"},
|
||||
want: []string{"compose", "version"},
|
||||
},
|
||||
{
|
||||
name: "compose -v",
|
||||
args: []string{"-v"},
|
||||
want: []string{"compose", "version"},
|
||||
},
|
||||
{
|
||||
name: "help",
|
||||
args: []string{"-h"},
|
||||
want: []string{"compose", "--help"},
|
||||
},
|
||||
{
|
||||
name: "issues/1962",
|
||||
args: []string{"psql", "-h", "postgres"},
|
||||
want: []string{"compose", "psql", "-h", "postgres"}, // -h should not be converted to --help
|
||||
},
|
||||
{
|
||||
name: "issues/8648",
|
||||
args: []string{"exec", "mongo", "mongo", "--host", "mongo"},
|
||||
want: []string{"compose", "exec", "mongo", "mongo", "--host", "mongo"}, // --host is passed to exec
|
||||
},
|
||||
{
|
||||
name: "issues/12",
|
||||
args: []string{"--log-level", "INFO", "up"},
|
||||
want: []string{"--log-level", "INFO", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "empty string argument",
|
||||
args: []string{"--project-directory", "", "ps"},
|
||||
want: []string{"compose", "--project-directory", "", "ps"},
|
||||
},
|
||||
{
|
||||
name: "compose as project name",
|
||||
args: []string{"--project-name", "compose", "down", "--remove-orphans"},
|
||||
want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"},
|
||||
},
|
||||
{
|
||||
name: "completion command",
|
||||
args: []string{"__complete", "up"},
|
||||
want: []string{"__complete", "compose", "up"},
|
||||
},
|
||||
{
|
||||
name: "string flag without argument",
|
||||
args: []string{"--log-level"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
if os.Getenv("BE_CRASHER") == "1" {
|
||||
Convert(tt.args)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^"+t.Name()+"$")
|
||||
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
|
||||
err := cmd.Run()
|
||||
var e *exec.ExitError
|
||||
if errors.As(err, &e) && !e.Success() {
|
||||
return
|
||||
}
|
||||
t.Fatalf("process ran with err %v, want exit status 1", err)
|
||||
} else {
|
||||
got := Convert(tt.args)
|
||||
assert.DeepEqual(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
|
||||
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 (
|
||||
"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 {
|
||||
cmd := &cobra.Command{
|
||||
Short: "Experimental commands",
|
||||
Use: "alpha [COMMAND]",
|
||||
Hidden: true,
|
||||
Annotations: map[string]string{
|
||||
"experimentalCLI": "true",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
vizCommand(p, dockerCli, backend),
|
||||
publishCommand(p, dockerCli, backend),
|
||||
generateCommand(p, backend),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type attachOpts struct {
|
||||
*composeOptions
|
||||
|
||||
service string
|
||||
index int
|
||||
|
||||
detachKeys string
|
||||
noStdin bool
|
||||
proxy bool
|
||||
}
|
||||
|
||||
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := attachOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
},
|
||||
}
|
||||
runCmd := &cobra.Command{
|
||||
Use: "attach [OPTIONS] SERVICE",
|
||||
Short: "Attach local standard input, output, and error streams to a service's running container",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
opts.service = args[0]
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runAttach(ctx, dockerCli, backend, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas.")
|
||||
runCmd.Flags().StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching from a container.")
|
||||
|
||||
runCmd.Flags().BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
|
||||
runCmd.Flags().BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachOpts := api.AttachOptions{
|
||||
Service: opts.service,
|
||||
Index: opts.index,
|
||||
DetachKeys: opts.detachKeys,
|
||||
NoStdin: opts.noStdin,
|
||||
Proxy: opts.proxy,
|
||||
}
|
||||
return backend.Attach(ctx, projectName, attachOpts)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/bridge"
|
||||
)
|
||||
|
||||
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bridge CMD [OPTIONS]",
|
||||
Short: "Convert compose files into another model",
|
||||
TraverseChildren: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
convertCommand(p, dockerCli),
|
||||
transformersCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
convertOpts := bridge.ConvertOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runConvert(ctx, dockerCli, p, convertOpts)
|
||||
}),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
|
||||
flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
|
||||
flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
|
||||
project, _, err := p.ToProject(ctx, dockerCli, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bridge.Convert(ctx, dockerCli, project, opts)
|
||||
}
|
||||
|
||||
func transformersCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "transformations CMD [OPTIONS]",
|
||||
Short: "Manage transformation images",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
listTransformersCommand(dockerCli),
|
||||
createTransformerCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listTransformersCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := lsOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List available transformations",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
transformers, err := bridge.ListTransformers(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return displayTransformer(dockerCli, transformers, options)
|
||||
}),
|
||||
}
|
||||
cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]")
|
||||
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error {
|
||||
if options.Quiet {
|
||||
for _, t := range transformers {
|
||||
if len(t.RepoTags) > 0 {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0])
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), t.ID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return formatter.Print(transformers, options.Format, dockerCli.Out(),
|
||||
func(w io.Writer) {
|
||||
for _, img := range transformers {
|
||||
id := stringid.TruncateID(img.ID)
|
||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
||||
repo, tag := "<none>", "<none>"
|
||||
if len(img.RepoTags) > 0 {
|
||||
ref, err := reference.ParseDockerRef(img.RepoTags[0])
|
||||
if err == nil {
|
||||
// ParseDockerRef will reject a local image ID
|
||||
repo = reference.FamiliarName(ref)
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size)
|
||||
}
|
||||
},
|
||||
"IMAGE ID", "REPO", "TAGS", "SIZE")
|
||||
}
|
||||
|
||||
func createTransformerCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts bridge.CreateTransformerOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTION] PATH",
|
||||
Short: "Create a new transformation",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
opts.Dest = args[0]
|
||||
return bridge.CreateTransformer(ctx, dockerCli, opts)
|
||||
}),
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)")
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"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"
|
||||
)
|
||||
|
||||
type buildOptions struct {
|
||||
*ProjectOptions
|
||||
quiet bool
|
||||
pull bool
|
||||
push bool
|
||||
args []string
|
||||
noCache bool
|
||||
memory cliopts.MemBytes
|
||||
ssh string
|
||||
builder string
|
||||
deps bool
|
||||
print bool
|
||||
check bool
|
||||
sbom string
|
||||
provenance string
|
||||
}
|
||||
|
||||
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
|
||||
var SSHKeys []types.SSHKey
|
||||
if opts.ssh != "" {
|
||||
id, path, found := strings.Cut(opts.ssh, "=")
|
||||
if !found && id != "default" {
|
||||
return api.BuildOptions{}, fmt.Errorf("invalid ssh key %q", opts.ssh)
|
||||
}
|
||||
SSHKeys = append(SSHKeys, types.SSHKey{
|
||||
ID: id,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
builderName := opts.builder
|
||||
if builderName == "" {
|
||||
builderName = os.Getenv("BUILDX_BUILDER")
|
||||
}
|
||||
|
||||
uiMode := ui.Mode
|
||||
if uiMode == ui.ModeJSON {
|
||||
uiMode = "rawjson"
|
||||
}
|
||||
|
||||
return api.BuildOptions{
|
||||
Pull: opts.pull,
|
||||
Push: opts.push,
|
||||
Progress: uiMode,
|
||||
Args: types.NewMappingWithEquals(opts.args),
|
||||
NoCache: opts.noCache,
|
||||
Quiet: opts.quiet,
|
||||
Services: services,
|
||||
Deps: opts.deps,
|
||||
Memory: int64(opts.memory),
|
||||
Print: opts.print,
|
||||
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 {
|
||||
opts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "build [OPTIONS] [SERVICE...]",
|
||||
Short: "Build or rebuild services",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.quiet {
|
||||
ui.Mode = ui.ModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
|
||||
opts.ssh = "default"
|
||||
}
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.push, "push", false, "Push service images")
|
||||
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
|
||||
flags.Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
|
||||
flags.MarkHidden("compress") //nolint:errcheck
|
||||
flags.Bool("force-rm", true, "Always remove intermediate containers. DEPRECATED")
|
||||
flags.MarkHidden("force-rm") //nolint:errcheck
|
||||
flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the image")
|
||||
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
|
||||
flags.MarkHidden("no-rm") //nolint:errcheck
|
||||
flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
|
||||
flags.StringVar(&p.Progress, "progress", "", fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
flags.MarkHidden("progress") //nolint:errcheck
|
||||
flags.BoolVar(&opts.print, "print", false, "Print equivalent bake file")
|
||||
flags.BoolVar(&opts.check, "check", false, "Check build configuration")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
||||
apiBuildOptions.Attestations = true
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Build(ctx, project, apiBuildOptions)
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type commitOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
service string
|
||||
reference string
|
||||
|
||||
pause bool
|
||||
comment string
|
||||
author string
|
||||
changes opts.ListOpts
|
||||
|
||||
index int
|
||||
}
|
||||
|
||||
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := commitOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]",
|
||||
Short: "Create a new image from a service container's changes",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
options.service = args[0]
|
||||
if len(args) > 1 {
|
||||
options.reference = args[1]
|
||||
}
|
||||
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runCommit(ctx, dockerCli, backend, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
|
||||
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
||||
options.changes = opts.NewListOpts(nil)
|
||||
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error {
|
||||
projectName, err := options.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitOptions := api.CommitOptions{
|
||||
Service: options.service,
|
||||
Reference: options.reference,
|
||||
Pause: options.pause,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes,
|
||||
Index: options.index,
|
||||
}
|
||||
|
||||
return backend.Commit(ctx, projectName, commitOptions)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
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 (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// validArgsFn defines a completion func to be returned to fetch completion options
|
||||
type validArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
func noCompletion() validArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{}, cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
var values []string
|
||||
serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...)
|
||||
for _, s := range serviceNames {
|
||||
if toComplete == "" || strings.HasPrefix(s, toComplete) {
|
||||
values = append(values, s)
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := backend.List(cmd.Context(), api.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var values []string
|
||||
for _, stack := range list {
|
||||
if strings.HasPrefix(stack.Name, toComplete) {
|
||||
values = append(values, stack.Name)
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
allProfileNames := project.AllServices().GetProfiles()
|
||||
sort.Strings(allProfileNames)
|
||||
|
||||
var values []string
|
||||
for _, profileName := range allProfileNames {
|
||||
if strings.HasPrefix(profileName, toComplete) {
|
||||
values = append(values, profileName)
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func completeScaleArgs(cli command.Cli, p *ProjectOptions) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
completions, directive := completeServiceNames(cli, p)(cmd, args, toComplete)
|
||||
for i, completion := range completions {
|
||||
completions[i] = completion + "="
|
||||
}
|
||||
return completions, directive
|
||||
}
|
||||
}
|
|
@ -0,0 +1,719 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
"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/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"
|
||||
)
|
||||
|
||||
const (
|
||||
// ComposeParallelLimit set the limit running concurrent operation on docker engine
|
||||
ComposeParallelLimit = "COMPOSE_PARALLEL_LIMIT"
|
||||
// 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"
|
||||
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
|
||||
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
|
||||
// ComposeIgnoreOrphans ignore "orphaned" containers
|
||||
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
|
||||
// ComposeEnvFiles defines the env files to use if --env-file isn't used
|
||||
ComposeEnvFiles = "COMPOSE_ENV_FILES"
|
||||
// ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
|
||||
ComposeMenu = "COMPOSE_MENU"
|
||||
// ComposeProgress defines type of progress output, if --progress isn't used
|
||||
ComposeProgress = "COMPOSE_PROGRESS"
|
||||
)
|
||||
|
||||
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
|
||||
func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
|
||||
lines, err := kvfile.ParseFromReader(r, lookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse env_file %s: %w", filename, err)
|
||||
}
|
||||
for _, line := range lines {
|
||||
key, value, _ := strings.Cut(line, "=")
|
||||
vars[key] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Command defines a compose CLI command as a func with args
|
||||
type Command func(context.Context, []string) error
|
||||
|
||||
// CobraCommand defines a cobra command function
|
||||
type CobraCommand func(context.Context, *cobra.Command, []string) error
|
||||
|
||||
// AdaptCmd adapt a CobraCommand func to cobra library
|
||||
func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
|
||||
s := make(chan os.Signal, 1)
|
||||
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-s
|
||||
cancel()
|
||||
signal.Stop(s)
|
||||
close(s)
|
||||
}()
|
||||
|
||||
err := fn(ctx, cmd, args)
|
||||
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
|
||||
err = dockercli.StatusError{
|
||||
StatusCode: 130,
|
||||
}
|
||||
}
|
||||
if ui.Mode == ui.ModeJSON {
|
||||
err = makeJSONError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt a Command func to cobra library
|
||||
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
|
||||
return AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
return fn(ctx, args)
|
||||
})
|
||||
}
|
||||
|
||||
type ProjectOptions struct {
|
||||
ProjectName string
|
||||
Profiles []string
|
||||
ConfigPaths []string
|
||||
WorkDir string
|
||||
ProjectDir string
|
||||
EnvFiles []string
|
||||
Compatibility bool
|
||||
Progress string
|
||||
Offline bool
|
||||
All bool
|
||||
}
|
||||
|
||||
// ProjectFunc does stuff within a types.Project
|
||||
type ProjectFunc func(ctx context.Context, project *types.Project) error
|
||||
|
||||
// ProjectServicesFunc does stuff within a types.Project and a selection of services
|
||||
type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
|
||||
|
||||
// WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
|
||||
func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func(cmd *cobra.Command, args []string) error {
|
||||
return o.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
|
||||
return fn(ctx, project)
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
|
||||
|
||||
project, err = project.WithServicesEnvironmentResolved(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fn(ctx, project, args)
|
||||
})
|
||||
}
|
||||
|
||||
type jsonErrorData struct {
|
||||
Error bool `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func errorAsJSON(message string) string {
|
||||
errorMessage := &jsonErrorData{
|
||||
Error: true,
|
||||
Message: message,
|
||||
}
|
||||
marshal, err := json.Marshal(errorMessage)
|
||||
if err == nil {
|
||||
return string(marshal)
|
||||
} else {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
func makeJSONError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var statusErr dockercli.StatusError
|
||||
if errors.As(err, &statusErr) {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: statusErr.StatusCode,
|
||||
Status: errorAsJSON(statusErr.Status),
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s", errorAsJSON(err.Error()))
|
||||
}
|
||||
|
||||
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.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)")
|
||||
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
|
||||
f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
|
||||
_ = f.MarkHidden("workdir")
|
||||
}
|
||||
|
||||
// get default value for a command line flag that is set by a coma-separated value in environment variable
|
||||
func defaultStringArrayVar(env string) []string {
|
||||
return strings.FieldsFunc(os.Getenv(env), func(c rune) bool {
|
||||
return c == ','
|
||||
})
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
|
||||
name := o.ProjectName
|
||||
var project *types.Project
|
||||
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
|
||||
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
|
||||
if err != nil {
|
||||
envProjectName := os.Getenv(ComposeProjectName)
|
||||
if envProjectName != "" {
|
||||
return nil, envProjectName, nil
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
project = p
|
||||
name = p.Name
|
||||
}
|
||||
return project, name, nil
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
|
||||
if o.ProjectName != "" {
|
||||
return o.ProjectName, nil
|
||||
}
|
||||
|
||||
envProjectName := os.Getenv(ComposeProjectName)
|
||||
if envProjectName != "" {
|
||||
return envProjectName, nil
|
||||
}
|
||||
|
||||
project, _, err := o.ToProject(ctx, dockerCli, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return project.Name, nil
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
|
||||
remotes := o.remoteLoaders(dockerCli)
|
||||
for _, r := range remotes {
|
||||
po = append(po, cli.WithResourceLoader(r))
|
||||
}
|
||||
|
||||
options, err := o.toProjectOptions(po...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
|
||||
api.Separator = "_"
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
switch event {
|
||||
case "extends":
|
||||
metrics.CountExtends++
|
||||
case "include":
|
||||
paths := metadata["path"].(types.StringList)
|
||||
for _, path := range paths {
|
||||
var isRemote bool
|
||||
for _, r := range remotes {
|
||||
if r.Accept(path) {
|
||||
isRemote = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isRemote {
|
||||
metrics.CountIncludesRemote++
|
||||
} else {
|
||||
metrics.CountIncludesLocal++
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
|
||||
api.Separator = "_"
|
||||
}
|
||||
|
||||
project, err := options.LoadProject(ctx)
|
||||
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
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
|
||||
if o.Offline {
|
||||
return nil
|
||||
}
|
||||
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
|
||||
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
|
||||
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
|
||||
}
|
||||
|
||||
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))...)
|
||||
}
|
||||
|
||||
// PluginName is the name of the plugin
|
||||
const PluginName = "compose"
|
||||
|
||||
// RunningAsStandalone detects when running as a standalone program
|
||||
func RunningAsStandalone() bool {
|
||||
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
|
||||
}
|
||||
|
||||
// RootCommand returns the compose command with its child commands
|
||||
func RootCommand(dockerCli command.Cli, backend Backend) *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
|
||||
logrus.AddHook(logutil.NewFilter([]logrus.Level{
|
||||
logrus.WarnLevel,
|
||||
},
|
||||
"commandConn.CloseWrite:",
|
||||
"commandConn.CloseRead:",
|
||||
))
|
||||
|
||||
experiments := experimental.NewState()
|
||||
opts := ProjectOptions{}
|
||||
var (
|
||||
ansi string
|
||||
noAnsi bool
|
||||
verbose bool
|
||||
version bool
|
||||
parallel int
|
||||
dryRun bool
|
||||
)
|
||||
c := &cobra.Command{
|
||||
Short: "Docker Compose",
|
||||
Long: "Define and run multi-container applications with Docker",
|
||||
Use: PluginName,
|
||||
TraverseChildren: true,
|
||||
// By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
if version {
|
||||
return versionCommand(dockerCli).Execute()
|
||||
}
|
||||
_ = cmd.Help()
|
||||
return dockercli.StatusError{
|
||||
StatusCode: 1,
|
||||
Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
|
||||
}
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
parent := cmd.Root()
|
||||
|
||||
if parent != nil {
|
||||
parentPrerun := parent.PersistentPreRunE
|
||||
if parentPrerun != nil {
|
||||
err := parentPrerun(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if noAnsi {
|
||||
if ansi != "auto" {
|
||||
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
|
||||
}
|
||||
ansi = "never"
|
||||
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
|
||||
}
|
||||
if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
|
||||
ansi = v
|
||||
}
|
||||
formatter.SetANSIMode(dockerCli, ansi)
|
||||
|
||||
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
|
||||
ui.NoColor()
|
||||
formatter.SetANSIMode(dockerCli, formatter.Never)
|
||||
}
|
||||
|
||||
switch ansi {
|
||||
case "never":
|
||||
ui.Mode = ui.ModePlain
|
||||
case "always":
|
||||
ui.Mode = ui.ModeTTY
|
||||
}
|
||||
|
||||
switch opts.Progress {
|
||||
case "", ui.ModeAuto:
|
||||
if ansi == "never" {
|
||||
ui.Mode = ui.ModePlain
|
||||
}
|
||||
case ui.ModeTTY:
|
||||
if ansi == "never" {
|
||||
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
|
||||
}
|
||||
ui.Mode = ui.ModeTTY
|
||||
case ui.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
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||
default:
|
||||
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
|
||||
}
|
||||
|
||||
// (4) options validation / normalization
|
||||
if opts.WorkDir != "" {
|
||||
if opts.ProjectDir != "" {
|
||||
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
|
||||
}
|
||||
opts.ProjectDir = opts.WorkDir
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
|
||||
}
|
||||
for i, file := range opts.EnvFiles {
|
||||
if !filepath.IsAbs(file) {
|
||||
file, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.EnvFiles[i] = file
|
||||
}
|
||||
}
|
||||
|
||||
composeCmd := cmd
|
||||
for composeCmd.Name() != PluginName {
|
||||
if !composeCmd.HasParent() {
|
||||
return fmt.Errorf("error parsing command line, expected %q", PluginName)
|
||||
}
|
||||
composeCmd = composeCmd.Parent()
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv(ComposeParallelLimit); ok && !composeCmd.Flags().Changed("parallel") {
|
||||
i, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s must be an integer (found: %q)", ComposeParallelLimit, v)
|
||||
}
|
||||
parallel = i
|
||||
}
|
||||
if parallel > 0 {
|
||||
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
|
||||
backend.MaxConcurrency(parallel)
|
||||
}
|
||||
|
||||
// dry run detection
|
||||
ctx, err = backend.DryRunMode(ctx, dryRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
statsCommand(&opts, dockerCli),
|
||||
watchCommand(&opts, dockerCli, backend),
|
||||
publishCommand(&opts, dockerCli, backend),
|
||||
alphaCommand(&opts, dockerCli, backend),
|
||||
bridgeCommand(&opts, dockerCli),
|
||||
volumesCommand(&opts, dockerCli, backend),
|
||||
)
|
||||
|
||||
c.Flags().SetInterspersed(false)
|
||||
opts.addProjectFlags(c.Flags())
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"project-name",
|
||||
completeProjectNames(backend),
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"project-directory",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{}, cobra.ShellCompDirectiveFilterDirs
|
||||
},
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"file",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
|
||||
},
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"profile",
|
||||
completeProfileNames(dockerCli, &opts),
|
||||
)
|
||||
|
||||
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`)
|
||||
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
||||
c.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Execute command in dry run mode")
|
||||
c.Flags().MarkHidden("version") //nolint:errcheck
|
||||
c.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
|
||||
c.Flags().MarkHidden("no-ansi") //nolint:errcheck
|
||||
c.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
|
||||
c.Flags().MarkHidden("verbose") //nolint:errcheck
|
||||
return c
|
||||
}
|
||||
|
||||
func setEnvWithDotEnv(opts ProjectOptions) error {
|
||||
options, err := cli.NewProjectOptions(opts.ConfigPaths,
|
||||
cli.WithWorkingDirectory(opts.ProjectDir),
|
||||
cli.WithOsEnv,
|
||||
cli.WithEnvFiles(opts.EnvFiles...),
|
||||
cli.WithDotEnv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for k, v := range envFromFile {
|
||||
if _, ok := os.LookupEnv(k); !ok {
|
||||
if err = os.Setenv(k, v); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestFilterServices(t *testing.T) {
|
||||
p := &types.Project{
|
||||
Services: types.Services{
|
||||
"foo": {
|
||||
Name: "foo",
|
||||
Links: []string{"bar"},
|
||||
},
|
||||
"bar": {
|
||||
Name: "bar",
|
||||
DependsOn: map[string]types.ServiceDependency{
|
||||
"zot": {},
|
||||
},
|
||||
},
|
||||
"zot": {
|
||||
Name: "zot",
|
||||
},
|
||||
"qix": {
|
||||
Name: "qix",
|
||||
},
|
||||
},
|
||||
}
|
||||
p, err := p.WithSelectedServices([]string{"bar"})
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(p.Services), 2)
|
||||
_, err = p.GetService("bar")
|
||||
assert.NilError(t, err)
|
||||
_, err = p.GetService("zot")
|
||||
assert.NilError(t, err)
|
||||
}
|
|
@ -0,0 +1,517 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"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"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
)
|
||||
|
||||
type configOptions struct {
|
||||
*ProjectOptions
|
||||
Format string
|
||||
Output string
|
||||
quiet bool
|
||||
resolveImageDigests bool
|
||||
noInterpolate bool
|
||||
noNormalize bool
|
||||
noResolvePath bool
|
||||
noResolveEnv bool
|
||||
services bool
|
||||
volumes bool
|
||||
networks bool
|
||||
models bool
|
||||
profiles bool
|
||||
images bool
|
||||
hash string
|
||||
noConsistency bool
|
||||
variables bool
|
||||
environment bool
|
||||
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...)
|
||||
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()...)
|
||||
return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
|
||||
}
|
||||
|
||||
func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
|
||||
return []cli.ProjectOptionsFn{
|
||||
cli.WithInterpolation(!o.noInterpolate),
|
||||
cli.WithResolvedPaths(!o.noResolvePath),
|
||||
cli.WithNormalization(!o.noNormalize),
|
||||
cli.WithConsistency(!o.noConsistency),
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithDiscardEnvFile,
|
||||
}
|
||||
}
|
||||
|
||||
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
opts := configOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [OPTIONS] [SERVICE...]",
|
||||
Short: "Parse, resolve and render compose file in canonical format",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.quiet {
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
if p.Compatibility {
|
||||
opts.noNormalize = true
|
||||
}
|
||||
if opts.lockImageDigests {
|
||||
opts.resolveImageDigests = true
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.services {
|
||||
return runServices(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.volumes {
|
||||
return runVolumes(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.networks {
|
||||
return runNetworks(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.models {
|
||||
return runModels(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.hash != "" {
|
||||
return runHash(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.profiles {
|
||||
return runProfiles(ctx, dockerCli, opts, args)
|
||||
}
|
||||
if opts.images {
|
||||
return runConfigImages(ctx, dockerCli, opts, args)
|
||||
}
|
||||
if opts.variables {
|
||||
return runVariables(ctx, dockerCli, opts, args)
|
||||
}
|
||||
if opts.environment {
|
||||
return runEnvironment(ctx, dockerCli, opts, args)
|
||||
}
|
||||
|
||||
if opts.Format == "" {
|
||||
opts.Format = "yaml"
|
||||
}
|
||||
return runConfig(ctx, dockerCli, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]")
|
||||
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
|
||||
flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything")
|
||||
flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables")
|
||||
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model")
|
||||
flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths")
|
||||
flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
|
||||
flags.BoolVar(&opts.noResolveEnv, "no-env-resolution", false, "Don't resolve service env files")
|
||||
|
||||
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.")
|
||||
flags.BoolVar(&opts.variables, "variables", false, "Print model variables and default values.")
|
||||
flags.BoolVar(&opts.environment, "environment", false, "Print environment used for interpolation.")
|
||||
flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) (err error) {
|
||||
var content []byte
|
||||
if opts.noInterpolate {
|
||||
content, err = runConfigNoInterpolate(ctx, dockerCli, opts, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
content, err = runConfigInterpolate(ctx, dockerCli, opts, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.noInterpolate {
|
||||
content = escapeDollarSign(content)
|
||||
}
|
||||
|
||||
if opts.quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Output != "" && len(content) > 0 {
|
||||
return os.WriteFile(opts.Output, content, 0o666)
|
||||
}
|
||||
_, err = fmt.Fprint(dockerCli.Out(), string(content))
|
||||
return err
|
||||
}
|
||||
|
||||
func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.resolveImageDigests {
|
||||
project, err = project.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.noResolveEnv {
|
||||
project, err = project.WithServicesEnvironmentResolved(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.noConsistency {
|
||||
err := project.CheckContainerNameUnicity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.lockImageDigests {
|
||||
project = imagesOnly(project)
|
||||
}
|
||||
|
||||
var content []byte
|
||||
switch opts.Format {
|
||||
case "json":
|
||||
content, err = project.MarshalJSON()
|
||||
case "yaml":
|
||||
content, err = project.MarshalYAML()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format %q", opts.Format)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// imagesOnly return project with all attributes removed but service.images
|
||||
func imagesOnly(project *types.Project) *types.Project {
|
||||
digests := types.Services{}
|
||||
for name, config := range project.Services {
|
||||
digests[name] = types.ServiceConfig{
|
||||
Image: config.Image,
|
||||
}
|
||||
}
|
||||
project = &types.Project{Services: digests}
|
||||
return project
|
||||
}
|
||||
|
||||
func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
|
||||
// we can't use ToProject, so the model we render here is only partially resolved
|
||||
model, err := opts.ToModel(ctx, dockerCli, services)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.resolveImageDigests {
|
||||
err = resolveImageDigests(ctx, dockerCli, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.lockImageDigests {
|
||||
for key, e := range model {
|
||||
if key != "services" {
|
||||
delete(model, key)
|
||||
} else {
|
||||
for _, s := range e.(map[string]any) {
|
||||
service := s.(map[string]any)
|
||||
for key := range service {
|
||||
if key != "image" {
|
||||
delete(service, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatModel(model, opts.Format)
|
||||
}
|
||||
|
||||
func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[string]any) (err error) {
|
||||
// create a pseudo-project so we can rely on WithImagesResolved to resolve images
|
||||
p := &types.Project{
|
||||
Services: types.Services{},
|
||||
}
|
||||
services := model["services"].(map[string]any)
|
||||
for name, s := range services {
|
||||
service := s.(map[string]any)
|
||||
if image, ok := service["image"]; ok {
|
||||
p.Services[name] = types.ServiceConfig{
|
||||
Image: image.(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Collect image resolved with digest and update model accordingly
|
||||
for name, s := range services {
|
||||
service := s.(map[string]any)
|
||||
config := p.Services[name]
|
||||
if config.Image != "" {
|
||||
service["image"] = config.Image
|
||||
}
|
||||
services[name] = service
|
||||
}
|
||||
model["services"] = services
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatModel(model map[string]any, format string) (content []byte, err error) {
|
||||
switch format {
|
||||
case "json":
|
||||
content, err = json.MarshalIndent(model, "", " ")
|
||||
case "yaml":
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
encoder := yaml.NewEncoder(buf)
|
||||
encoder.SetIndent(2)
|
||||
err = encoder.Encode(model)
|
||||
content = buf.Bytes()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format %q", format)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
if opts.noInterpolate {
|
||||
// we can't use ToProject, so the model we render here is only partially resolved
|
||||
data, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := data["services"]; ok {
|
||||
for serviceName := range data["services"].(map[string]any) {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for n := range project.Volumes {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for n := range project.Networks {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
services = project.ServiceNames()
|
||||
}
|
||||
|
||||
sorted := services
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i] < sorted[j]
|
||||
})
|
||||
|
||||
for _, name := range sorted {
|
||||
s, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := compose.ServiceHash(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range project.AllServices() {
|
||||
for _, p := range s.Profiles {
|
||||
set[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
profiles := make([]string, 0, len(set))
|
||||
for p := range set {
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
sort.Strings(profiles)
|
||||
for _, p := range profiles {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range project.Services {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
opts.noInterpolate = true
|
||||
model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
variables := template.ExtractVariables(model, template.DefaultPattern)
|
||||
|
||||
if opts.Format == "yaml" {
|
||||
result, err := yaml.Marshal(variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(string(result))
|
||||
return nil
|
||||
}
|
||||
|
||||
return formatter.Print(variables, opts.Format, dockerCli.Out(), func(w io.Writer) {
|
||||
for name, variable := range variables {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%t\t%s\t%s\n", name, variable.Required, variable.DefaultValue, variable.PresenceValue)
|
||||
}
|
||||
}, "NAME", "REQUIRED", "DEFAULT VALUE", "ALTERNATE VALUE")
|
||||
}
|
||||
|
||||
func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range project.Environment.Values() {
|
||||
fmt.Println(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func escapeDollarSign(marshal []byte) []byte {
|
||||
dollar := []byte{'$'}
|
||||
escDollar := []byte{'$', '$'}
|
||||
return bytes.ReplaceAll(marshal, dollar, escDollar)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type copyOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
source string
|
||||
destination string
|
||||
index int
|
||||
all bool
|
||||
followLink bool
|
||||
copyUIDGID bool
|
||||
}
|
||||
|
||||
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := copyOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
copyCmd := &cobra.Command{
|
||||
Use: `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
|
||||
docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH`,
|
||||
Short: "Copy files/folders between a service container and the local filesystem",
|
||||
Args: cli.ExactArgs(2),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if args[0] == "" {
|
||||
return errors.New("source can not be empty")
|
||||
}
|
||||
if args[1] == "" {
|
||||
return errors.New("destination can not be empty")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
flags := copyCmd.Flags()
|
||||
flags.IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
|
||||
flags.BoolVar(&opts.all, "all", false, "Include containers created by the run command")
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
|
||||
return copyCmd
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
|
||||
name, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Copy(ctx, name, api.CopyOptions{
|
||||
Source: opts.source,
|
||||
Destination: opts.destination,
|
||||
All: opts.all,
|
||||
Index: opts.index,
|
||||
FollowLink: opts.followLink,
|
||||
CopyUIDGID: opts.copyUIDGID,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
Build bool
|
||||
noBuild bool
|
||||
Pull string
|
||||
pullChanged bool
|
||||
removeOrphans bool
|
||||
ignoreOrphans bool
|
||||
forceRecreate bool
|
||||
noRecreate bool
|
||||
recreateDeps bool
|
||||
noInherit bool
|
||||
timeChanged bool
|
||||
timeout int
|
||||
quietPull bool
|
||||
scale []string
|
||||
AssumeYes bool
|
||||
}
|
||||
|
||||
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := createOptions{}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] [SERVICE...]",
|
||||
Short: "Creates containers for a service",
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.pullChanged = cmd.Flags().Changed("pull")
|
||||
if opts.Build && opts.noBuild {
|
||||
return fmt.Errorf("--build and --no-build are incompatible")
|
||||
}
|
||||
if opts.forceRecreate && opts.noRecreate {
|
||||
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
|
||||
}
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers")
|
||||
flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy")
|
||||
flags.StringVar(&opts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never"|"build")`)
|
||||
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information")
|
||||
flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed")
|
||||
flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
|
||||
flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
|
||||
flags.BoolVarP(&opts.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
// assumeYes was introduced by mistake as `--y`
|
||||
if name == "y" {
|
||||
logrus.Warn("--y is deprecated, please use --yes instead")
|
||||
name = "yes"
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
|
||||
if err := createOpts.Apply(project); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var build *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
bo, err := buildOpts.toAPIBuildOptions(services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
build = &bo
|
||||
}
|
||||
|
||||
return backend.Create(ctx, project, api.CreateOptions{
|
||||
Build: build,
|
||||
Services: services,
|
||||
RemoveOrphans: createOpts.removeOrphans,
|
||||
IgnoreOrphans: createOpts.ignoreOrphans,
|
||||
Recreate: createOpts.recreateStrategy(),
|
||||
RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
|
||||
Inherit: !createOpts.noInherit,
|
||||
Timeout: createOpts.GetTimeout(),
|
||||
QuietPull: createOpts.quietPull,
|
||||
AssumeYes: createOpts.AssumeYes,
|
||||
})
|
||||
}
|
||||
|
||||
func (opts createOptions) recreateStrategy() string {
|
||||
if opts.noRecreate {
|
||||
return api.RecreateNever
|
||||
}
|
||||
if opts.forceRecreate {
|
||||
return api.RecreateForce
|
||||
}
|
||||
if opts.noInherit {
|
||||
return api.RecreateForce
|
||||
}
|
||||
return api.RecreateDiverged
|
||||
}
|
||||
|
||||
func (opts createOptions) dependenciesRecreateStrategy() string {
|
||||
if opts.noRecreate {
|
||||
return api.RecreateNever
|
||||
}
|
||||
if opts.recreateDeps {
|
||||
return api.RecreateForce
|
||||
}
|
||||
return api.RecreateDiverged
|
||||
}
|
||||
|
||||
func (opts createOptions) GetTimeout() *time.Duration {
|
||||
if opts.timeChanged {
|
||||
t := time.Duration(opts.timeout) * time.Second
|
||||
return &t
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts createOptions) Apply(project *types.Project) error {
|
||||
if opts.pullChanged {
|
||||
if !opts.isPullPolicyValid() {
|
||||
return fmt.Errorf("invalid --pull option %q", opts.Pull)
|
||||
}
|
||||
for i, service := range project.Services {
|
||||
service.PullPolicy = opts.Pull
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
// N.B. opts.Build means "force build all", but images can still be built
|
||||
// when this is false
|
||||
// e.g. if a service has pull_policy: build or its local image is policy
|
||||
if opts.Build {
|
||||
for i, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := applyScaleOpts(project, opts.scale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyScaleOpts(project *types.Project, opts []string) error {
|
||||
for _, scale := range opts {
|
||||
split := strings.Split(scale, "=")
|
||||
if len(split) != 2 {
|
||||
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
|
||||
}
|
||||
name := split[0]
|
||||
replicas, err := strconv.Atoi(split[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setServiceScale(project, name, replicas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts createOptions) isPullPolicyValid() bool {
|
||||
pullPolicies := []string{
|
||||
types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild,
|
||||
types.PullPolicyMissing, types.PullPolicyIfNotPresent,
|
||||
}
|
||||
return slices.Contains(pullPolicies, opts.Pull)
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
type downOptions struct {
|
||||
*ProjectOptions
|
||||
removeOrphans bool
|
||||
timeChanged bool
|
||||
timeout int
|
||||
volumes bool
|
||||
images string
|
||||
}
|
||||
|
||||
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := downOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
downCmd := &cobra.Command{
|
||||
Use: "down [OPTIONS] [SERVICES]",
|
||||
Short: "Stop and remove containers, networks",
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
if opts.images != "" {
|
||||
if opts.images != "all" && opts.images != "local" {
|
||||
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runDown(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: noCompletion(),
|
||||
}
|
||||
flags := downCmd.Flags()
|
||||
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers`)
|
||||
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
if name == "volume" {
|
||||
name = "volumes"
|
||||
logrus.Warn("--volume is deprecated, please use --volumes")
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
return downCmd
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var timeout *time.Duration
|
||||
if opts.timeChanged {
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
return backend.Down(ctx, name, api.DownOptions{
|
||||
RemoveOrphans: opts.removeOrphans,
|
||||
Project: project,
|
||||
Timeout: timeout,
|
||||
Images: opts.images,
|
||||
Volumes: opts.volumes,
|
||||
Services: services,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type eventsOpts struct {
|
||||
*composeOptions
|
||||
json bool
|
||||
since string
|
||||
until string
|
||||
}
|
||||
|
||||
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := eventsOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
},
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
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)
|
||||
}),
|
||||
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 {
|
||||
name, err := opts.toProjectName(ctx, dockerCli)
|
||||
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{}{
|
||||
"time": event.Timestamp,
|
||||
"type": "container",
|
||||
"service": event.Service,
|
||||
"id": event.Container,
|
||||
"action": event.Status,
|
||||
"attributes": event.Attributes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), string(marshal))
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), event)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type execOpts struct {
|
||||
*composeOptions
|
||||
|
||||
service string
|
||||
command []string
|
||||
environment []string
|
||||
workingDir string
|
||||
|
||||
noTty bool
|
||||
user string
|
||||
detach bool
|
||||
index int
|
||||
privileged bool
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := execOpts{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
},
|
||||
}
|
||||
runCmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]",
|
||||
Short: "Execute a command in a running container",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
opts.service = args[0]
|
||||
opts.command = args[1:]
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
err := runExec(ctx, dockerCli, backend, opts)
|
||||
if err != nil {
|
||||
logrus.Debugf("%v", err)
|
||||
var cliError cli.StatusError
|
||||
if ok := errors.As(err, &cliError); ok {
|
||||
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
|
||||
}
|
||||
}
|
||||
return err
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background")
|
||||
runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
|
||||
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().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")
|
||||
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
|
||||
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY")
|
||||
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
runCmd.Flags().SetInterspersed(false)
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectOptions, err := opts.composeOptions.toProjectOptions() //nolint:staticcheck
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lookupFn := func(k string) (string, bool) {
|
||||
v, ok := projectOptions.Environment[k]
|
||||
return v, ok
|
||||
}
|
||||
execOpts := api.RunOptions{
|
||||
Service: opts.service,
|
||||
Command: opts.command,
|
||||
Environment: compose.ToMobyEnv(types.NewMappingWithEquals(opts.environment).Resolve(lookupFn)),
|
||||
Tty: !opts.noTty,
|
||||
User: opts.user,
|
||||
Privileged: opts.privileged,
|
||||
Index: opts.index,
|
||||
Detach: opts.detach,
|
||||
WorkingDir: opts.workingDir,
|
||||
Interactive: opts.interactive,
|
||||
}
|
||||
|
||||
exitCode, err := backend.Exec(ctx, projectName, execOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := fmt.Sprintf("exit status %d", exitCode)
|
||||
if err != nil && err.Error() != "" {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type exportOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
service string
|
||||
output string
|
||||
index int
|
||||
}
|
||||
|
||||
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := exportOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "export [OPTIONS] SERVICE",
|
||||
Short: "Export a service container's filesystem as a tar archive",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
options.service = args[0]
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runExport(ctx, dockerCli, backend, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
|
||||
flags.StringVarP(&options.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
|
||||
projectName, err := options.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exportOptions := api.ExportOptions{
|
||||
Service: options.service,
|
||||
Index: options.index,
|
||||
Output: options.output,
|
||||
}
|
||||
|
||||
return backend.Export(ctx, projectName, exportOptions)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
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"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type generateOptions struct {
|
||||
*ProjectOptions
|
||||
Format string
|
||||
}
|
||||
|
||||
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := generateOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate [OPTIONS] [CONTAINERS...]",
|
||||
Short: "EXPERIMENTAL - Generate a Compose file from existing containers",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runGenerate(ctx, backend, opts, args)
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&opts.ProjectName, "name", "", "Project name to set in the Compose file")
|
||||
cmd.Flags().StringVar(&opts.ProjectDir, "project-dir", "", "Directory to use for the project")
|
||||
cmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenerate(ctx context.Context, backend api.Service, 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")
|
||||
}
|
||||
project, err := backend.Generate(ctx, api.GenerateOptions{
|
||||
Containers: containers,
|
||||
ProjectName: opts.ProjectName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var content []byte
|
||||
switch opts.Format {
|
||||
case "json":
|
||||
content, err = project.MarshalJSON()
|
||||
case "yaml":
|
||||
content, err = project.MarshalYAML()
|
||||
default:
|
||||
return fmt.Errorf("unsupported format %q", opts.Format)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(content))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type imageOptions struct {
|
||||
*ProjectOptions
|
||||
Quiet bool
|
||||
Format string
|
||||
}
|
||||
|
||||
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := imageOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
imgCmd := &cobra.Command{
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
|
||||
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
return imgCmd
|
||||
}
|
||||
|
||||
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
images, err := backend.Images(ctx, projectName, api.ImagesOptions{
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Quiet {
|
||||
ids := []string{}
|
||||
for _, img := range images {
|
||||
id := img.ID
|
||||
if i := strings.IndexRune(img.ID, ':'); i >= 0 {
|
||||
id = id[i+1:]
|
||||
}
|
||||
if !slices.Contains(ids, id) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
for _, img := range ids {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), img)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
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"`
|
||||
}
|
||||
// Convert map to slice
|
||||
var imageList []img
|
||||
for ctr, i := range images {
|
||||
imageList = append(imageList, img{
|
||||
ContainerName: ctr,
|
||||
ID: i.ID,
|
||||
Repository: i.Repository,
|
||||
Tag: i.Tag,
|
||||
Platform: platforms.Format(i.Platform),
|
||||
Size: i.Size,
|
||||
LastTagTime: i.LastTagTime,
|
||||
})
|
||||
}
|
||||
json, err := formatter.ToJSON(imageList, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(dockerCli.Out(), json)
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Print(images, opts.Format, dockerCli.Out(),
|
||||
func(w io.Writer) {
|
||||
for _, container := range slices.Sorted(maps.Keys(images)) {
|
||||
img := images[container]
|
||||
id := stringid.TruncateID(img.ID)
|
||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
||||
repo := img.Repository
|
||||
if repo == "" {
|
||||
repo = "<none>"
|
||||
}
|
||||
tag := img.Tag
|
||||
if tag == "" {
|
||||
tag = "<none>"
|
||||
}
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " 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)
|
||||
}
|
||||
},
|
||||
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED")
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type killOptions struct {
|
||||
*ProjectOptions
|
||||
removeOrphans bool
|
||||
signal string
|
||||
}
|
||||
|
||||
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := killOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file")
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, 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{
|
||||
RemoveOrphans: opts.removeOrphans,
|
||||
Project: project,
|
||||
Services: services,
|
||||
Signal: opts.signal,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
)
|
||||
|
||||
type lsOptions struct {
|
||||
Format string
|
||||
Quiet bool
|
||||
All bool
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func listCommand(dockerCli command.Cli, backend api.Service) *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)
|
||||
}),
|
||||
Args: cobra.NoArgs,
|
||||
ValidArgsFunction: noCompletion(),
|
||||
}
|
||||
lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json]")
|
||||
lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display project names")
|
||||
lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided")
|
||||
lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects")
|
||||
|
||||
return lsCmd
|
||||
}
|
||||
|
||||
var acceptedListFilters = map[string]bool{
|
||||
"name": true,
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error {
|
||||
filters := lsOpts.Filter.Value()
|
||||
err := filters.Validate(acceptedListFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filters.Len() > 0 {
|
||||
var filtered []api.Stack
|
||||
for _, s := range stackList {
|
||||
if filters.Contains("name") && !filters.Match("name", s.Name) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
stackList = filtered
|
||||
}
|
||||
|
||||
if lsOpts.Quiet {
|
||||
for _, s := range stackList {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), s.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
view := viewFromStackList(stackList)
|
||||
return formatter.Print(view, lsOpts.Format, dockerCli.Out(), func(w io.Writer) {
|
||||
for _, stack := range view {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles)
|
||||
}
|
||||
}, "NAME", "STATUS", "CONFIG FILES")
|
||||
}
|
||||
|
||||
type stackView struct {
|
||||
Name string
|
||||
Status string
|
||||
ConfigFiles string
|
||||
}
|
||||
|
||||
func viewFromStackList(stackList []api.Stack) []stackView {
|
||||
retList := make([]stackView, len(stackList))
|
||||
for i, s := range stackList {
|
||||
retList[i] = stackView{
|
||||
Name: s.Name,
|
||||
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
|
||||
ConfigFiles: s.ConfigFiles,
|
||||
}
|
||||
}
|
||||
return retList
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type logsOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
follow bool
|
||||
index int
|
||||
tail string
|
||||
since string
|
||||
until string
|
||||
noColor bool
|
||||
noPrefix bool
|
||||
timestamps bool
|
||||
}
|
||||
|
||||
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := logsOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
logsCmd := &cobra.Command{
|
||||
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)
|
||||
}),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.index > 0 && len(args) != 1 {
|
||||
return errors.New("--index requires one service to be selected")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := logsCmd.Flags()
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||
flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
|
||||
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output")
|
||||
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs")
|
||||
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
|
||||
flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container")
|
||||
return logsCmd
|
||||
}
|
||||
|
||||
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// exclude services configured to ignore output (attach: false), until explicitly selected
|
||||
if project != nil && len(services) == 0 {
|
||||
for n, service := range project.Services {
|
||||
if service.Attach == nil || *service.Attach {
|
||||
services = append(services, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
|
||||
return backend.Logs(ctx, name, consumer, api.LogOptions{
|
||||
Project: project,
|
||||
Services: services,
|
||||
Follow: opts.follow,
|
||||
Index: opts.index,
|
||||
Tail: opts.tail,
|
||||
Since: opts.since,
|
||||
Until: opts.until,
|
||||
Timestamps: opts.timestamps,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
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"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"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"
|
||||
)
|
||||
|
||||
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
defaultPlatform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
|
||||
for name, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// default platform only applies if the service doesn't specify
|
||||
if defaultPlatform != "" && service.Platform == "" {
|
||||
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) {
|
||||
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
|
||||
}
|
||||
service.Platform = defaultPlatform
|
||||
}
|
||||
|
||||
if service.Platform != "" {
|
||||
if len(service.Build.Platforms) > 0 {
|
||||
if !slices.Contains(service.Build.Platforms, service.Platform) {
|
||||
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
if buildForSinglePlatform || len(service.Build.Platforms) == 0 {
|
||||
// if we're building for a single platform, we want to build for the platform we'll use to run the image
|
||||
// similarly, if no build platforms were explicitly specified, it makes sense to build for the platform
|
||||
// the image is designed for rather than allowing the builder to infer the platform
|
||||
service.Build.Platforms = []string{service.Platform}
|
||||
}
|
||||
}
|
||||
|
||||
// services can specify that they should be built for multiple platforms, which can be used
|
||||
// with `docker compose build` to produce a multi-arch image
|
||||
// other cases, such as `up` and `run`, need a single architecture to actually run
|
||||
// if there is only a single platform present (which might have been inferred
|
||||
// from service.Platform above), it will be used, even if it requires emulation.
|
||||
// if there's more than one platform, then the list is cleared so that the builder
|
||||
// can decide.
|
||||
// TODO(milas): there's no validation that the platform the builder will pick is actually one
|
||||
// of the supported platforms from the build definition
|
||||
// e.g. `build.platforms: [linux/arm64, linux/amd64]` on a `linux/ppc64` machine would build
|
||||
// for `linux/ppc64` instead of returning an error that it's not a valid platform for the service.
|
||||
if buildForSinglePlatform && len(service.Build.Platforms) > 1 {
|
||||
// empty indicates that the builder gets to decide
|
||||
service.Build.Platforms = nil
|
||||
}
|
||||
project.Services[name] = service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git)
|
||||
func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool {
|
||||
if len(options.ConfigPaths) == 0 {
|
||||
return false
|
||||
}
|
||||
remoteLoaders := options.remoteLoaders(dockerCli)
|
||||
for _, loader := range remoteLoaders {
|
||||
if loader.Accept(options.ConfigPaths[0]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checksForRemoteStack handles environment variable prompts for remote configurations
|
||||
func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error {
|
||||
if !isRemoteConfig(dockerCli, options) {
|
||||
return nil
|
||||
}
|
||||
if metrics, ok := ctx.Value(tracing.MetricsKey{}).(tracing.Metrics); ok && metrics.CountIncludesRemote > 0 {
|
||||
if err := confirmRemoteIncludes(dockerCli, options, assumeYes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
displayLocationRemoteStack(dockerCli, project, options)
|
||||
return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
|
||||
}
|
||||
|
||||
// Prepare the values map and collect all variables info
|
||||
type varInfo struct {
|
||||
name string
|
||||
value string
|
||||
source string
|
||||
required bool
|
||||
defaultValue string
|
||||
}
|
||||
|
||||
// promptForInterpolatedVariables displays all variables and their values at once,
|
||||
// then prompts for confirmation
|
||||
func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error {
|
||||
if assumeYes {
|
||||
return nil
|
||||
}
|
||||
|
||||
varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if noVariables {
|
||||
return nil
|
||||
}
|
||||
|
||||
displayInterpolationVariables(dockerCli.Out(), varsInfo)
|
||||
|
||||
// Prompt for confirmation
|
||||
userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out())
|
||||
msg := "\nDo you want to proceed with these variables? [Y/n]: "
|
||||
confirmed, err := userInput.Confirm(msg, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return fmt.Errorf("operation cancelled by user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) {
|
||||
cmdEnvMap := extractEnvCLIDefined(cmdEnvs)
|
||||
|
||||
// Create a model without interpolation to extract variables
|
||||
opts := configOptions{
|
||||
noInterpolate: true,
|
||||
ProjectOptions: projectOptions,
|
||||
}
|
||||
|
||||
model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Extract variables that need interpolation
|
||||
variables := template.ExtractVariables(model, template.DefaultPattern)
|
||||
if len(variables) == 0 {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
var varsInfo []varInfo
|
||||
proposedValues := make(map[string]string)
|
||||
|
||||
for name, variable := range variables {
|
||||
info := varInfo{
|
||||
name: name,
|
||||
required: variable.Required,
|
||||
defaultValue: variable.DefaultValue,
|
||||
}
|
||||
|
||||
// Determine value and source based on priority
|
||||
if value, exists := cmdEnvMap[name]; exists {
|
||||
info.value = value
|
||||
info.source = "command-line"
|
||||
proposedValues[name] = value
|
||||
} else if value, exists := os.LookupEnv(name); exists {
|
||||
info.value = value
|
||||
info.source = "environment"
|
||||
proposedValues[name] = value
|
||||
} else if variable.DefaultValue != "" {
|
||||
info.value = variable.DefaultValue
|
||||
info.source = "compose file"
|
||||
proposedValues[name] = variable.DefaultValue
|
||||
} else {
|
||||
info.value = "<unset>"
|
||||
info.source = "none"
|
||||
}
|
||||
|
||||
varsInfo = append(varsInfo, info)
|
||||
}
|
||||
return varsInfo, false, nil
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
return cmdEnvMap
|
||||
}
|
||||
|
||||
func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
|
||||
// Display all variables in a table format
|
||||
_, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:")
|
||||
|
||||
w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT")
|
||||
sort.Slice(varsInfo, func(a, b int) bool {
|
||||
return varsInfo[a].name < varsInfo[b].name
|
||||
})
|
||||
for _, info := range varsInfo {
|
||||
required := "no"
|
||||
if info.required {
|
||||
required = "yes"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
||||
info.name,
|
||||
info.value,
|
||||
info.source,
|
||||
required,
|
||||
info.defaultValue,
|
||||
)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
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 {
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
|
||||
}
|
||||
}
|
||||
|
||||
func confirmRemoteIncludes(dockerCli command.Cli, options buildOptions, assumeYes bool) error {
|
||||
if assumeYes {
|
||||
return nil
|
||||
}
|
||||
|
||||
var remoteIncludes []string
|
||||
remoteLoaders := options.ProjectOptions.remoteLoaders(dockerCli) //nolint:staticcheck
|
||||
for _, cf := range options.ProjectOptions.ConfigPaths { //nolint:staticcheck
|
||||
for _, loader := range remoteLoaders {
|
||||
if loader.Accept(cf) {
|
||||
remoteIncludes = append(remoteIncludes, cf)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(remoteIncludes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), "\nWarning: This Compose project includes files from remote sources:")
|
||||
for _, include := range remoteIncludes {
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), " - %s\n", include)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), "\nRemote includes could potentially be malicious. Make sure you trust the source.")
|
||||
|
||||
msg := "Do you want to continue? [y/N]: "
|
||||
confirmed, err := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm(msg, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirmed {
|
||||
return fmt.Errorf("operation cancelled by user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Services: types.Services{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"alice/32",
|
||||
},
|
||||
},
|
||||
Platform: "alice/32",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, true))
|
||||
require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, false))
|
||||
require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
|
||||
project.Services["test"].Build.Platforms)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
|
||||
},
|
||||
Services: types.Services{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, true))
|
||||
require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, false))
|
||||
require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
|
||||
project.Services["test"].Build.Platforms)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
|
||||
},
|
||||
Services: types.Services{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.EqualError(t, applyPlatforms(project, true),
|
||||
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.EqualError(t, applyPlatforms(project, false),
|
||||
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsRemoteConfig(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configPaths []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty config paths",
|
||||
configPaths: []string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "local file",
|
||||
configPaths: []string{"docker-compose.yaml"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "OCI reference",
|
||||
configPaths: []string{"oci://registry.example.com/stack:latest"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "GIT reference",
|
||||
configPaths: []string{"git://github.com/user/repo.git"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: tt.configPaths,
|
||||
},
|
||||
}
|
||||
got := isRemoteConfig(cli, opts)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayLocationRemoteStack(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
|
||||
|
||||
project := &types.Project{
|
||||
Name: "test-project",
|
||||
WorkingDir: "/tmp/test",
|
||||
}
|
||||
|
||||
options := buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
|
||||
},
|
||||
}
|
||||
|
||||
displayLocationRemoteStack(cli, project, options)
|
||||
|
||||
output := buf.String()
|
||||
require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
|
||||
}
|
||||
|
||||
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) }()
|
||||
|
||||
// Create a temporary compose file
|
||||
composeContent := `
|
||||
services:
|
||||
app:
|
||||
image: nginx
|
||||
environment:
|
||||
- TEST_VAR=${TEST_VAR:?required} # required with default
|
||||
- API_KEY=${API_KEY:?} # required without default
|
||||
- DEBUG=${DEBUG:-true} # optional with default
|
||||
- UNSET_VAR # optional without default
|
||||
`
|
||||
composePath := filepath.Join(tmpDir, "docker-compose.yml")
|
||||
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
|
||||
|
||||
// Create ProjectOptions with the temporary compose file
|
||||
projectOptions := &ProjectOptions{
|
||||
ConfigPaths: []string{composePath},
|
||||
}
|
||||
|
||||
// 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")
|
||||
}()
|
||||
|
||||
// Extract variables from the model
|
||||
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, noVariables)
|
||||
|
||||
// Display the variables
|
||||
displayInterpolationVariables(cli.Out(), info)
|
||||
|
||||
// Expected output format with proper spacing
|
||||
expected := "\nFound the following variables in configuration:\n" +
|
||||
"VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
|
||||
"API_KEY 123456 environment yes \n" +
|
||||
"DEBUG true compose file no true\n" +
|
||||
"TEST_VAR test-value environment yes \n"
|
||||
|
||||
// Normalize spaces and newlines for comparison
|
||||
normalizeSpaces := func(s string) string {
|
||||
// Replace multiple spaces with a single space
|
||||
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
|
||||
return s
|
||||
}
|
||||
|
||||
actualOutput := buf.String()
|
||||
|
||||
// Compare normalized strings
|
||||
require.Equal(t,
|
||||
normalizeSpaces(expected),
|
||||
normalizeSpaces(actualOutput),
|
||||
"\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
|
||||
}
|
||||
|
||||
func TestConfirmRemoteIncludes(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts buildOptions
|
||||
assumeYes bool
|
||||
userInput string
|
||||
wantErr bool
|
||||
errMessage string
|
||||
wantPrompt bool
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
name: "no remote includes",
|
||||
opts: buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: []string{
|
||||
"docker-compose.yaml",
|
||||
"./local/path/compose.yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
assumeYes: false,
|
||||
wantErr: false,
|
||||
wantPrompt: false,
|
||||
},
|
||||
{
|
||||
name: "assume yes with remote includes",
|
||||
opts: buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: []string{
|
||||
"oci://registry.example.com/stack:latest",
|
||||
"git://github.com/user/repo.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
assumeYes: true,
|
||||
wantErr: false,
|
||||
wantPrompt: false,
|
||||
},
|
||||
{
|
||||
name: "user confirms remote includes",
|
||||
opts: buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: []string{
|
||||
"oci://registry.example.com/stack:latest",
|
||||
"git://github.com/user/repo.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
assumeYes: false,
|
||||
userInput: "y\n",
|
||||
wantErr: false,
|
||||
wantPrompt: true,
|
||||
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
|
||||
" - oci://registry.example.com/stack:latest\n" +
|
||||
" - git://github.com/user/repo.git\n" +
|
||||
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
|
||||
"Do you want to continue? [y/N]: ",
|
||||
},
|
||||
{
|
||||
name: "user rejects remote includes",
|
||||
opts: buildOptions{
|
||||
ProjectOptions: &ProjectOptions{
|
||||
ConfigPaths: []string{
|
||||
"oci://registry.example.com/stack:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
assumeYes: false,
|
||||
userInput: "n\n",
|
||||
wantErr: true,
|
||||
errMessage: "operation cancelled by user",
|
||||
wantPrompt: true,
|
||||
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
|
||||
" - oci://registry.example.com/stack:latest\n" +
|
||||
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
|
||||
"Do you want to continue? [y/N]: ",
|
||||
},
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
|
||||
|
||||
if tt.wantPrompt {
|
||||
inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
|
||||
cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
|
||||
}
|
||||
|
||||
err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.errMessage, err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.wantOutput != "" {
|
||||
require.Equal(t, tt.wantOutput, buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type pauseOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := pauseOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "pause [SERVICE...]",
|
||||
Short: "Pause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPause(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Pause(ctx, name, api.PauseOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
}
|
||||
|
||||
type unpauseOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := unpauseOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "unpause [SERVICE...]",
|
||||
Short: "Unpause services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runUnPause(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.UnPause(ctx, name, api.PauseOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type portOptions struct {
|
||||
*ProjectOptions
|
||||
port uint16
|
||||
protocol string
|
||||
index int
|
||||
}
|
||||
|
||||
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := portOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "port [OPTIONS] SERVICE PRIVATE_PORT",
|
||||
Short: "Print the public port for a port binding",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
port, err := strconv.ParseUint(args[1], 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.port = uint16(port)
|
||||
opts.protocol = strings.ToLower(opts.protocol)
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPort(ctx, dockerCli, backend, opts, args[0])
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
|
||||
cmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
|
||||
Protocol: opts.protocol,
|
||||
Index: opts.index,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"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"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
*ProjectOptions
|
||||
Format string
|
||||
All bool
|
||||
Quiet bool
|
||||
Services bool
|
||||
Filter string
|
||||
Status []string
|
||||
noTrunc bool
|
||||
Orphans bool
|
||||
}
|
||||
|
||||
func (p *psOptions) parseFilter() error {
|
||||
if p.Filter == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitN(p.Filter, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return errors.New("arguments to --filter should be in form KEY=VAL")
|
||||
}
|
||||
switch parts[0] {
|
||||
case "status":
|
||||
p.Status = append(p.Status, parts[1])
|
||||
case "source":
|
||||
return api.ErrNotImplemented
|
||||
default:
|
||||
return fmt.Errorf("unknown filter %s", parts[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := psOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
psCmd := &cobra.Command{
|
||||
Use: "ps [OPTIONS] [SERVICE...]",
|
||||
Short: "List containers",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return opts.parseFilter()
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPs(ctx, dockerCli, backend, args, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := psCmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
|
||||
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status)")
|
||||
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.BoolVar(&opts.Services, "services", false, "Display services")
|
||||
flags.BoolVar(&opts.Orphans, "orphans", true, "Include orphaned services (not declared by project)")
|
||||
flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
return psCmd
|
||||
}
|
||||
|
||||
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if project != nil {
|
||||
names := project.ServiceNames()
|
||||
if len(services) > 0 {
|
||||
for _, service := range services {
|
||||
if !slices.Contains(names, service) {
|
||||
return fmt.Errorf("no such service: %s", service)
|
||||
}
|
||||
}
|
||||
} else if !opts.Orphans {
|
||||
// until user asks to list orphaned services, we only include those declared in project
|
||||
services = names
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := backend.Ps(ctx, name, api.PsOptions{
|
||||
Project: project,
|
||||
All: opts.All || len(opts.Status) != 0,
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(opts.Status) != 0 {
|
||||
containers = filterByStatus(containers, opts.Status)
|
||||
}
|
||||
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
return containers[i].Name < containers[j].Name
|
||||
})
|
||||
|
||||
if opts.Quiet {
|
||||
for _, c := range containers {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), c.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Services {
|
||||
services := []string{}
|
||||
for _, c := range containers {
|
||||
s := c.Service
|
||||
if !slices.Contains(services, s) {
|
||||
services = append(services, s)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Format == "" {
|
||||
opts.Format = dockerCli.ConfigFile().PsFormat
|
||||
}
|
||||
|
||||
containerCtx := cliformatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
|
||||
Trunc: !opts.noTrunc,
|
||||
}
|
||||
return formatter.ContainerWrite(containerCtx, containers)
|
||||
}
|
||||
|
||||
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
|
||||
var filtered []api.ContainerSummary
|
||||
for _, c := range containers {
|
||||
if hasStatus(c, statuses) {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func hasStatus(c api.ContainerSummary, statuses []string) bool {
|
||||
for _, status := range statuses {
|
||||
if c.State == status {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
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")
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type publishOptions struct {
|
||||
*ProjectOptions
|
||||
resolveImageDigests bool
|
||||
ociVersion string
|
||||
withEnvironment bool
|
||||
assumeYes bool
|
||||
}
|
||||
|
||||
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := publishOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
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])
|
||||
}),
|
||||
Args: cli.ExactArgs(1),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
|
||||
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.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
// assumeYes was introduced by mistake as `--y`
|
||||
if name == "y" {
|
||||
logrus.Warn("--y is deprecated, please use --yes instead")
|
||||
name = "yes"
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if metrics.CountIncludesLocal > 0 {
|
||||
return errors.New("cannot publish compose file with local includes")
|
||||
}
|
||||
|
||||
return backend.Publish(ctx, project, repository, api.PublishOptions{
|
||||
ResolveImageDigests: opts.resolveImageDigests,
|
||||
OCIVersion: api.OCIVersion(opts.ociVersion),
|
||||
WithEnvironment: opts.withEnvironment,
|
||||
AssumeYes: opts.assumeYes,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type pullOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
quiet bool
|
||||
parallel bool
|
||||
noParallel bool
|
||||
includeDeps bool
|
||||
ignorePullFailures bool
|
||||
noBuildable bool
|
||||
policy string
|
||||
}
|
||||
|
||||
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := pullOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "pull [OPTIONS] [SERVICE...]",
|
||||
Short: "Pull service images",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("no-parallel") {
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
|
||||
}
|
||||
if cmd.Flags().Changed("parallel") {
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPull(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information")
|
||||
cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies")
|
||||
cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel")
|
||||
flags.MarkHidden("parallel") //nolint:errcheck
|
||||
cmd.Flags().BoolVar(&opts.noParallel, "no-parallel", true, "DEPRECATED disable parallel pulling")
|
||||
flags.MarkHidden("no-parallel") //nolint:errcheck
|
||||
cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures")
|
||||
cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built")
|
||||
cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always")`)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (opts pullOptions) apply(project *types.Project, services []string) (*types.Project, error) {
|
||||
if !opts.includeDeps {
|
||||
var err error
|
||||
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.policy != "" {
|
||||
for i, service := range project.Services {
|
||||
if service.Image == "" {
|
||||
continue
|
||||
}
|
||||
service.PullPolicy = opts.policy
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err = opts.apply(project, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Pull(ctx, project, api.PullOptions{
|
||||
Quiet: opts.quiet,
|
||||
IgnoreFailures: opts.ignorePullFailures,
|
||||
IgnoreBuildable: opts.noBuildable,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestApplyPullOptions(t *testing.T) {
|
||||
project := &types.Project{
|
||||
Services: types.Services{
|
||||
"must-build": {
|
||||
Name: "must-build",
|
||||
// No image, local build only
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
},
|
||||
},
|
||||
"has-build": {
|
||||
Name: "has-build",
|
||||
Image: "registry.example.com/myservice",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
},
|
||||
},
|
||||
"must-pull": {
|
||||
Name: "must-pull",
|
||||
Image: "registry.example.com/another-service",
|
||||
},
|
||||
},
|
||||
}
|
||||
project, err := pullOptions{
|
||||
policy: types.PullPolicyMissing,
|
||||
}.apply(project, nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, project.Services["must-build"].PullPolicy, "") // still default
|
||||
assert.Equal(t, project.Services["has-build"].PullPolicy, types.PullPolicyMissing)
|
||||
assert.Equal(t, project.Services["must-pull"].PullPolicy, types.PullPolicyMissing)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type pushOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
IncludeDeps bool
|
||||
Ignorefailures bool
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := pushOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
pushCmd := &cobra.Command{
|
||||
Use: "push [OPTIONS] [SERVICE...]",
|
||||
Short: "Push service images",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPush(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
|
||||
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
|
||||
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.IncludeDeps {
|
||||
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return backend.Push(ctx, project, api.PushOptions{
|
||||
IgnoreFailures: opts.Ignorefailures,
|
||||
Quiet: opts.Quiet,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
*ProjectOptions
|
||||
force bool
|
||||
stop bool
|
||||
volumes bool
|
||||
}
|
||||
|
||||
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := removeOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] [SERVICE...]",
|
||||
Short: "Removes stopped service containers",
|
||||
Long: `Removes stopped service containers
|
||||
|
||||
By default, anonymous volumes attached to containers will not be removed. You
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
f := cmd.Flags()
|
||||
f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal")
|
||||
f.BoolVarP(&opts.stop, "stop", "s", false, "Stop the containers, if required, before removing")
|
||||
f.BoolVarP(&opts.volumes, "volumes", "v", false, "Remove any anonymous volumes attached to containers")
|
||||
f.BoolP("all", "a", false, "Deprecated - no effect")
|
||||
f.MarkHidden("all") //nolint:errcheck
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, 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{
|
||||
Services: services,
|
||||
Force: opts.force,
|
||||
Volumes: opts.volumes,
|
||||
Project: project,
|
||||
Stop: opts.stop,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type restartOptions struct {
|
||||
*ProjectOptions
|
||||
timeChanged bool
|
||||
timeout int
|
||||
noDeps bool
|
||||
}
|
||||
|
||||
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := restartOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
restartCmd := &cobra.Command{
|
||||
Use: "restart [OPTIONS] [SERVICE...]",
|
||||
Short: "Restart service containers",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRestart(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := restartCmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services")
|
||||
|
||||
return restartCmd
|
||||
}
|
||||
|
||||
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if project != nil && len(services) > 0 {
|
||||
project, err = project.WithServicesEnabled(services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var timeout *time.Duration
|
||||
if opts.timeChanged {
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
|
||||
return backend.Restart(ctx, name, api.RestartOptions{
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
Project: project,
|
||||
NoDeps: opts.noDeps,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"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"
|
||||
)
|
||||
|
||||
type runOptions struct {
|
||||
*composeOptions
|
||||
Service string
|
||||
Command []string
|
||||
environment []string
|
||||
envFiles []string
|
||||
Detach bool
|
||||
Remove bool
|
||||
noTty bool
|
||||
tty bool
|
||||
interactive bool
|
||||
user string
|
||||
workdir string
|
||||
entrypoint string
|
||||
entrypointCmd []string
|
||||
capAdd opts.ListOpts
|
||||
capDrop opts.ListOpts
|
||||
labels []string
|
||||
volumes []string
|
||||
publish []string
|
||||
useAliases bool
|
||||
servicePorts bool
|
||||
name string
|
||||
noDeps bool
|
||||
ignoreOrphans bool
|
||||
removeOrphans bool
|
||||
quiet bool
|
||||
quietPull bool
|
||||
}
|
||||
|
||||
func (options runOptions) apply(project *types.Project) (*types.Project, error) {
|
||||
if options.noDeps {
|
||||
var err error
|
||||
project, err = project.WithSelectedServices([]string{options.Service}, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
target, err := project.GetService(options.Service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
target.Tty = !options.noTty
|
||||
target.StdinOpen = options.interactive
|
||||
|
||||
// --service-ports and --publish are incompatible
|
||||
if !options.servicePorts {
|
||||
if len(target.Ports) > 0 {
|
||||
logrus.Debug("Running service without ports exposed as --service-ports=false")
|
||||
}
|
||||
target.Ports = []types.ServicePortConfig{}
|
||||
for _, p := range options.publish {
|
||||
config, err := types.ParsePortConfig(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target.Ports = append(target.Ports, config...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range options.volumes {
|
||||
volume, err := format.ParseVolume(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target.Volumes = append(target.Volumes, volume)
|
||||
}
|
||||
|
||||
for name := range project.Services {
|
||||
if name == options.Service {
|
||||
project.Services[name] = target
|
||||
break
|
||||
}
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (options runOptions) getEnvironment() (types.Mapping, error) {
|
||||
environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).ToMapping()
|
||||
for _, file := range options.envFiles {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vars, err := dotenv.ParseWithLookup(f, func(k string) (string, bool) {
|
||||
value, ok := environment[k]
|
||||
return value, ok
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
for k, v := range vars {
|
||||
if _, ok := environment[k]; !ok {
|
||||
environment[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return environment, nil
|
||||
}
|
||||
|
||||
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := runOptions{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
},
|
||||
capAdd: opts.NewListOpts(nil),
|
||||
capDrop: opts.NewListOpts(nil),
|
||||
}
|
||||
createOpts := createOptions{}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
|
||||
Short: "Run a one-off command on a service",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
options.Service = args[0]
|
||||
if len(args) > 1 {
|
||||
options.Command = args[1:]
|
||||
}
|
||||
if len(options.publish) > 0 && options.servicePorts {
|
||||
return fmt.Errorf("--service-ports and --publish are incompatible")
|
||||
}
|
||||
if cmd.Flags().Changed("entrypoint") {
|
||||
command, err := shellwords.Parse(options.entrypoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.entrypointCmd = command
|
||||
}
|
||||
if cmd.Flags().Changed("tty") {
|
||||
if cmd.Flags().Changed("no-TTY") {
|
||||
return fmt.Errorf("--tty and --no-TTY can't be used together")
|
||||
} else {
|
||||
options.noTty = !options.tty
|
||||
}
|
||||
}
|
||||
if options.quiet {
|
||||
progress.Mode = progress.ModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err = project.WithServicesEnvironmentResolved(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createOpts.quietPull {
|
||||
buildOpts.Progress = string(xprogress.QuietMode)
|
||||
}
|
||||
|
||||
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
|
||||
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
|
||||
flags.StringArrayVar(&options.envFiles, "env-from-file", []string{}, "Set environment variables from file")
|
||||
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
|
||||
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
|
||||
flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)")
|
||||
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
|
||||
flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
|
||||
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
|
||||
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
|
||||
flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services")
|
||||
flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume")
|
||||
flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host")
|
||||
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to")
|
||||
flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host")
|
||||
flags.StringVar(&createOpts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process")
|
||||
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information")
|
||||
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container")
|
||||
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().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
flags.SetNormalizeFunc(normalizeRunFlags)
|
||||
flags.SetInterspersed(false)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
switch name {
|
||||
case "volumes":
|
||||
name = "volume"
|
||||
case "labels":
|
||||
name = "label"
|
||||
}
|
||||
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 {
|
||||
project, err := options.apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createOpts.Apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checksForRemoteStack(ctx, dockerCli, project, buildOpts, createOpts.AssumeYes, []string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labels := types.Labels{}
|
||||
for _, s := range options.labels {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("label must be set as KEY=VALUE")
|
||||
}
|
||||
labels[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
var buildForRun *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
bo, err := buildOpts.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildForRun = &bo
|
||||
}
|
||||
|
||||
environment, err := options.getEnvironment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start container and attach to container streams
|
||||
runOpts := api.RunOptions{
|
||||
CreateOptions: api.CreateOptions{
|
||||
Build: buildForRun,
|
||||
RemoveOrphans: options.removeOrphans,
|
||||
IgnoreOrphans: options.ignoreOrphans,
|
||||
QuietPull: options.quietPull,
|
||||
},
|
||||
Name: options.name,
|
||||
Service: options.Service,
|
||||
Command: options.Command,
|
||||
Detach: options.Detach,
|
||||
AutoRemove: options.Remove,
|
||||
Tty: !options.noTty,
|
||||
Interactive: options.interactive,
|
||||
WorkingDir: options.workdir,
|
||||
User: options.user,
|
||||
CapAdd: options.capAdd.GetSlice(),
|
||||
CapDrop: options.capDrop.GetSlice(),
|
||||
Environment: environment.Values(),
|
||||
Entrypoint: options.entrypointCmd,
|
||||
Labels: labels,
|
||||
UseNetworkAliases: options.useAliases,
|
||||
NoDeps: options.noDeps,
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
for name, service := range project.Services {
|
||||
if name == options.Service {
|
||||
service.StdinOpen = options.interactive
|
||||
project.Services[name] = service
|
||||
}
|
||||
}
|
||||
|
||||
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type scaleOptions struct {
|
||||
*ProjectOptions
|
||||
noDeps bool
|
||||
}
|
||||
|
||||
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := scaleOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
scaleCmd := &cobra.Command{
|
||||
Use: "scale [SERVICE=REPLICAS...]",
|
||||
Short: "Scale services ",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
serviceTuples, err := parseServicesReplicasArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runScale(ctx, dockerCli, backend, opts, serviceTuples)
|
||||
}),
|
||||
ValidArgsFunction: completeScaleArgs(dockerCli, p),
|
||||
}
|
||||
flags := scaleCmd.Flags()
|
||||
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services")
|
||||
|
||||
return scaleCmd
|
||||
}
|
||||
|
||||
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
|
||||
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.noDeps {
|
||||
if project, err = project.WithSelectedServices(services, types.IgnoreDependencies); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range serviceReplicaTuples {
|
||||
service, err := project.GetService(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.SetScale(value)
|
||||
project.Services[key] = service
|
||||
}
|
||||
|
||||
return backend.Scale(ctx, project, api.ScaleOptions{Services: services})
|
||||
}
|
||||
|
||||
func parseServicesReplicasArgs(args []string) (map[string]int, error) {
|
||||
serviceReplicaTuples := map[string]int{}
|
||||
for _, arg := range args {
|
||||
key, val, ok := strings.Cut(arg, "=")
|
||||
if !ok || key == "" || val == "" {
|
||||
return nil, fmt.Errorf("invalid scale specifier: %s", arg)
|
||||
}
|
||||
intValue, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scale specifier: can't parse replica value as int: %v", arg)
|
||||
}
|
||||
serviceReplicaTuples[key] = intValue
|
||||
}
|
||||
return serviceReplicaTuples, nil
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type startOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := startOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
startCmd := &cobra.Command{
|
||||
Use: "start [SERVICE...]",
|
||||
Short: "Start services",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStart(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return startCmd
|
||||
}
|
||||
|
||||
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Start(ctx, name, api.StartOptions{
|
||||
AttachTo: services,
|
||||
Project: project,
|
||||
Services: services,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type statsOptions struct {
|
||||
ProjectOptions *ProjectOptions
|
||||
all bool
|
||||
format string
|
||||
noStream bool
|
||||
noTrunc bool
|
||||
}
|
||||
|
||||
func statsCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
opts := statsOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "stats [OPTIONS] [SERVICE]",
|
||||
Short: "Display a live stream of container(s) resource usage statistics",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStats(ctx, dockerCli, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||
flags.StringVar(&opts.format, "format", "", `Format output using a custom template:
|
||||
'table': Print output in table format with column headers (default)
|
||||
'table TEMPLATE': Print output in table format using the given Go template
|
||||
'json': Print in JSON format
|
||||
'TEMPLATE': Print output using the given Go template.
|
||||
Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates`)
|
||||
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error {
|
||||
name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter := []filters.KeyValuePair{
|
||||
filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, name)),
|
||||
}
|
||||
if len(service) > 0 {
|
||||
filter = append(filter, filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, service[0])))
|
||||
}
|
||||
args := filters.NewArgs(filter...)
|
||||
return container.RunStats(ctx, dockerCli, &container.StatsOptions{
|
||||
All: opts.all,
|
||||
NoStream: opts.noStream,
|
||||
NoTrunc: opts.noTrunc,
|
||||
Format: opts.format,
|
||||
Filters: &args,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type stopOptions struct {
|
||||
*ProjectOptions
|
||||
timeChanged bool
|
||||
timeout int
|
||||
}
|
||||
|
||||
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := stopOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop [OPTIONS] [SERVICE...]",
|
||||
Short: "Stop services",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStop(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var timeout *time.Duration
|
||||
if opts.timeChanged {
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
return backend.Stop(ctx, name, api.StopOptions{
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type topOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := topOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
topCmd := &cobra.Command{
|
||||
Use: "top [SERVICES...]",
|
||||
Short: "Display the running processes",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runTop(ctx, dockerCli, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
return topCmd
|
||||
}
|
||||
|
||||
type (
|
||||
topHeader map[string]int // maps a proc title to its output index
|
||||
topEntries map[string]string
|
||||
)
|
||||
|
||||
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
|
||||
projectName, err := opts.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers, err := backend.Top(ctx, projectName, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
return containers[i].Name < containers[j].Name
|
||||
})
|
||||
|
||||
header, entries := collectTop(containers)
|
||||
return topPrint(dockerCli.Out(), header, entries)
|
||||
}
|
||||
|
||||
func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
|
||||
// map column name to its header (should keep working if backend.Top returns
|
||||
// varying columns for different containers)
|
||||
header := topHeader{"SERVICE": 0, "#": 1}
|
||||
|
||||
// assume one process per container and grow if needed
|
||||
entries := make([]topEntries, 0, len(containers))
|
||||
|
||||
for _, container := range containers {
|
||||
for _, proc := range container.Processes {
|
||||
entry := topEntries{
|
||||
"SERVICE": container.Service,
|
||||
"#": container.Replica,
|
||||
}
|
||||
for i, title := range container.Titles {
|
||||
if _, exists := header[title]; !exists {
|
||||
header[title] = len(header)
|
||||
}
|
||||
entry[title] = proc[i]
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure CMD is the right-most column
|
||||
if pos, ok := header["CMD"]; ok {
|
||||
maxPos := pos
|
||||
for h, i := range header {
|
||||
if i > maxPos {
|
||||
maxPos = i
|
||||
}
|
||||
if i > pos {
|
||||
header[h] = i - 1
|
||||
}
|
||||
}
|
||||
header["CMD"] = maxPos
|
||||
}
|
||||
|
||||
return header, entries
|
||||
}
|
||||
|
||||
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(out, 4, 1, 2, ' ', 0)
|
||||
|
||||
// write headers in the order we've encountered them
|
||||
h := make([]string, len(headers))
|
||||
for title, index := range headers {
|
||||
h[index] = title
|
||||
}
|
||||
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))
|
||||
|
||||
for _, row := range rows {
|
||||
// write proc data in header order
|
||||
r := make([]string, len(headers))
|
||||
for title, index := range headers {
|
||||
if v, ok := row[title]; ok {
|
||||
r[index] = v
|
||||
} else {
|
||||
r[index] = "-"
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
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 compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var topTestCases = []struct {
|
||||
name string
|
||||
titles []string
|
||||
procs [][]string
|
||||
|
||||
header topHeader
|
||||
entries []topEntries
|
||||
output string
|
||||
}{
|
||||
{
|
||||
name: "noprocs",
|
||||
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
|
||||
procs: [][]string{},
|
||||
header: topHeader{"SERVICE": 0, "#": 1},
|
||||
entries: []topEntries{},
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
|
||||
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
|
||||
header: topHeader{
|
||||
"SERVICE": 0,
|
||||
"#": 1,
|
||||
"UID": 2,
|
||||
"PID": 3,
|
||||
"PPID": 4,
|
||||
"C": 5,
|
||||
"STIME": 6,
|
||||
"TTY": 7,
|
||||
"TIME": 8,
|
||||
"CMD": 9,
|
||||
},
|
||||
entries: []topEntries{
|
||||
{
|
||||
"SERVICE": "simple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:01",
|
||||
"CMD": "/entrypoint",
|
||||
},
|
||||
},
|
||||
output: trim(`
|
||||
SERVICE # UID PID PPID C STIME TTY TIME CMD
|
||||
simple 1 root 1 1 0 12:00 ? 00:00:01 /entrypoint
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "noppid",
|
||||
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
|
||||
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
|
||||
header: topHeader{
|
||||
"SERVICE": 0,
|
||||
"#": 1,
|
||||
"UID": 2,
|
||||
"PID": 3,
|
||||
"C": 4,
|
||||
"STIME": 5,
|
||||
"TTY": 6,
|
||||
"TIME": 7,
|
||||
"CMD": 8,
|
||||
},
|
||||
entries: []topEntries{
|
||||
{
|
||||
"SERVICE": "noppid",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:02",
|
||||
"CMD": "/entrypoint",
|
||||
},
|
||||
},
|
||||
output: trim(`
|
||||
SERVICE # UID PID C STIME TTY TIME CMD
|
||||
noppid 1 root 1 0 12:00 ? 00:00:02 /entrypoint
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "extra-hdr",
|
||||
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
|
||||
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
|
||||
header: topHeader{
|
||||
"SERVICE": 0,
|
||||
"#": 1,
|
||||
"UID": 2,
|
||||
"GID": 3,
|
||||
"PID": 4,
|
||||
"PPID": 5,
|
||||
"C": 6,
|
||||
"STIME": 7,
|
||||
"TTY": 8,
|
||||
"TIME": 9,
|
||||
"CMD": 10,
|
||||
},
|
||||
entries: []topEntries{
|
||||
{
|
||||
"SERVICE": "extra-hdr",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"GID": "1",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:03",
|
||||
"CMD": "/entrypoint",
|
||||
},
|
||||
},
|
||||
output: trim(`
|
||||
SERVICE # UID GID PID PPID C STIME TTY TIME CMD
|
||||
extra-hdr 1 root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
|
||||
procs: [][]string{
|
||||
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
|
||||
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
|
||||
},
|
||||
header: topHeader{
|
||||
"SERVICE": 0,
|
||||
"#": 1,
|
||||
"UID": 2,
|
||||
"PID": 3,
|
||||
"PPID": 4,
|
||||
"C": 5,
|
||||
"STIME": 6,
|
||||
"TTY": 7,
|
||||
"TIME": 8,
|
||||
"CMD": 9,
|
||||
},
|
||||
entries: []topEntries{
|
||||
{
|
||||
"SERVICE": "multiple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:04",
|
||||
"CMD": "/entrypoint",
|
||||
},
|
||||
{
|
||||
"SERVICE": "multiple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "123",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:42",
|
||||
"CMD": "sleep infinity",
|
||||
},
|
||||
},
|
||||
output: trim(`
|
||||
SERVICE # UID PID PPID C STIME TTY TIME CMD
|
||||
multiple 1 root 1 1 0 12:00 ? 00:00:04 /entrypoint
|
||||
multiple 1 root 123 1 0 12:00 ? 00:00:42 sleep infinity
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
// TestRunTopCore only tests the core functionality of runTop: formatting
|
||||
// and printing of the output of (api.Service).Top().
|
||||
func TestRunTopCore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
all := []api.ContainerProcSummary{}
|
||||
|
||||
for _, tc := range topTestCases {
|
||||
summary := api.ContainerProcSummary{
|
||||
Name: "not used",
|
||||
Titles: tc.titles,
|
||||
Processes: tc.procs,
|
||||
Service: tc.name,
|
||||
Replica: "1",
|
||||
}
|
||||
all = append(all, summary)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
header, entries := collectTop([]api.ContainerProcSummary{summary})
|
||||
assert.Equal(t, tc.header, header)
|
||||
assert.Equal(t, tc.entries, entries)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := topPrint(&buf, header, entries)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.output, buf.String())
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("all", func(t *testing.T) {
|
||||
header, entries := collectTop(all)
|
||||
assert.Equal(t, topHeader{
|
||||
"SERVICE": 0,
|
||||
"#": 1,
|
||||
"UID": 2,
|
||||
"PID": 3,
|
||||
"PPID": 4,
|
||||
"C": 5,
|
||||
"STIME": 6,
|
||||
"TTY": 7,
|
||||
"TIME": 8,
|
||||
"GID": 9,
|
||||
"CMD": 10,
|
||||
}, header)
|
||||
assert.Equal(t, []topEntries{
|
||||
{
|
||||
"SERVICE": "simple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:01",
|
||||
"CMD": "/entrypoint",
|
||||
}, {
|
||||
"SERVICE": "noppid",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:02",
|
||||
"CMD": "/entrypoint",
|
||||
}, {
|
||||
"SERVICE": "extra-hdr",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"GID": "1",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:03",
|
||||
"CMD": "/entrypoint",
|
||||
}, {
|
||||
"SERVICE": "multiple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "1",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:04",
|
||||
"CMD": "/entrypoint",
|
||||
}, {
|
||||
"SERVICE": "multiple",
|
||||
"#": "1",
|
||||
"UID": "root",
|
||||
"PID": "123",
|
||||
"PPID": "1",
|
||||
"C": "0",
|
||||
"STIME": "12:00",
|
||||
"TTY": "?",
|
||||
"TIME": "00:00:42",
|
||||
"CMD": "sleep infinity",
|
||||
},
|
||||
}, entries)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := topPrint(&buf, header, entries)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, trim(`
|
||||
SERVICE # UID PID PPID C STIME TTY TIME GID CMD
|
||||
simple 1 root 1 1 0 12:00 ? 00:00:01 - /entrypoint
|
||||
noppid 1 root 1 - 0 12:00 ? 00:00:02 - /entrypoint
|
||||
extra-hdr 1 root 1 1 0 12:00 ? 00:00:03 1 /entrypoint
|
||||
multiple 1 root 1 1 0 12:00 ? 00:00:04 - /entrypoint
|
||||
multiple 1 root 123 1 0 12:00 ? 00:00:42 - sleep infinity
|
||||
`), buf.String())
|
||||
})
|
||||
}
|
||||
|
||||
func trim(s string) string {
|
||||
var out bytes.Buffer
|
||||
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
out.WriteString(strings.TrimSpace(line))
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
return out.String()
|
||||
}
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
xprogress "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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"
|
||||
)
|
||||
|
||||
// composeOptions hold options common to `up` and `run` to run compose project
|
||||
type composeOptions struct {
|
||||
*ProjectOptions
|
||||
}
|
||||
|
||||
type upOptions struct {
|
||||
*composeOptions
|
||||
Detach bool
|
||||
noStart bool
|
||||
noDeps bool
|
||||
cascadeStop bool
|
||||
cascadeFail bool
|
||||
exitCodeFrom string
|
||||
noColor bool
|
||||
noPrefix bool
|
||||
attachDependencies bool
|
||||
attach []string
|
||||
noAttach []string
|
||||
timestamp bool
|
||||
wait bool
|
||||
waitTimeout int
|
||||
watch bool
|
||||
navigationMenu bool
|
||||
navigationMenuChanged bool
|
||||
}
|
||||
|
||||
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
|
||||
if opts.noDeps {
|
||||
var err error
|
||||
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.exitCodeFrom != "" {
|
||||
_, err := project.GetService(opts.exitCodeFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) {
|
||||
if !dockerCli.Out().IsTerminal() {
|
||||
opts.navigationMenu = false
|
||||
return
|
||||
}
|
||||
// If --menu flag was not set
|
||||
if !opts.navigationMenuChanged {
|
||||
if envVar, ok := os.LookupEnv(ComposeMenu); ok {
|
||||
opts.navigationMenu = utils.StringToBool(envVar)
|
||||
return
|
||||
}
|
||||
// ...and COMPOSE_MENU env var is not defined we want the default value to be true
|
||||
opts.navigationMenu = true
|
||||
}
|
||||
}
|
||||
|
||||
func (opts upOptions) OnExit() api.Cascade {
|
||||
switch {
|
||||
case opts.cascadeStop:
|
||||
return api.CascadeStop
|
||||
case opts.cascadeFail:
|
||||
return api.CascadeFail
|
||||
default:
|
||||
return api.CascadeIgnore
|
||||
}
|
||||
}
|
||||
|
||||
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
up := upOptions{}
|
||||
create := createOptions{}
|
||||
build := buildOptions{ProjectOptions: p}
|
||||
upCmd := &cobra.Command{
|
||||
Use: "up [OPTIONS] [SERVICE...]",
|
||||
Short: "Create and start containers",
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
create.pullChanged = cmd.Flags().Changed("pull")
|
||||
create.timeChanged = cmd.Flags().Changed("timeout")
|
||||
up.navigationMenuChanged = cmd.Flags().Changed("menu")
|
||||
if !cmd.Flags().Changed("remove-orphans") {
|
||||
create.removeOrphans = utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
}
|
||||
return validateFlags(&up, &create)
|
||||
}),
|
||||
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
|
||||
create.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
if create.ignoreOrphans && create.removeOrphans {
|
||||
return fmt.Errorf("cannot combine %s and --remove-orphans", ComposeIgnoreOrphans)
|
||||
}
|
||||
if len(up.attach) != 0 && up.attachDependencies {
|
||||
return errors.New("cannot combine --attach and --attach-dependencies")
|
||||
}
|
||||
|
||||
up.validateNavigationMenu(dockerCli)
|
||||
|
||||
if !p.All && len(project.Services) == 0 {
|
||||
return fmt.Errorf("no service selected")
|
||||
}
|
||||
|
||||
return runUp(ctx, dockerCli, backend, create, up, build, project, services)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := upCmd.Flags()
|
||||
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
|
||||
flags.BoolVar(&create.Build, "build", false, "Build images before starting containers")
|
||||
flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy")
|
||||
flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
|
||||
flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
|
||||
flags.StringArrayVar(&create.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
|
||||
flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output")
|
||||
flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs")
|
||||
flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed")
|
||||
flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
|
||||
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them")
|
||||
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
|
||||
flags.BoolVar(&up.cascadeFail, "abort-on-container-failure", false, "Stops all containers if any container exited with failure. Incompatible with -d")
|
||||
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
|
||||
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running")
|
||||
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps")
|
||||
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services")
|
||||
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")
|
||||
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
|
||||
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
|
||||
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
|
||||
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
|
||||
flags.BoolVarP(&create.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
// assumeYes was introduced by mistake as `--y`
|
||||
if name == "y" {
|
||||
logrus.Warn("--y is deprecated, please use --yes instead")
|
||||
name = "yes"
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
})
|
||||
return upCmd
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func validateFlags(up *upOptions, create *createOptions) error {
|
||||
if up.exitCodeFrom != "" && !up.cascadeFail {
|
||||
up.cascadeStop = true
|
||||
}
|
||||
if up.cascadeStop && up.cascadeFail {
|
||||
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
|
||||
}
|
||||
if up.wait {
|
||||
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
|
||||
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
|
||||
}
|
||||
up.Detach = true
|
||||
}
|
||||
if create.Build && create.noBuild {
|
||||
return fmt.Errorf("--build and --no-build are incompatible")
|
||||
}
|
||||
if up.Detach && (up.attachDependencies || up.cascadeStop || up.cascadeFail || len(up.attach) > 0 || up.watch) {
|
||||
if up.wait {
|
||||
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
|
||||
} else {
|
||||
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
|
||||
}
|
||||
}
|
||||
if create.noInherit && create.noRecreate {
|
||||
return fmt.Errorf("--no-recreate and --renew-anon-volumes are incompatible")
|
||||
}
|
||||
if create.forceRecreate && create.noRecreate {
|
||||
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
|
||||
}
|
||||
if create.recreateDeps && create.noRecreate {
|
||||
return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
|
||||
}
|
||||
if create.noBuild && up.watch {
|
||||
return fmt.Errorf("--no-build and --watch are incompatible")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func runUp(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
backend api.Service,
|
||||
createOptions createOptions,
|
||||
upOptions upOptions,
|
||||
buildOptions buildOptions,
|
||||
project *types.Project,
|
||||
services []string,
|
||||
) error {
|
||||
if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := createOptions.Apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err = upOptions.apply(project, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var build *api.BuildOptions
|
||||
if !createOptions.noBuild {
|
||||
if createOptions.quietPull {
|
||||
buildOptions.Progress = string(xprogress.QuietMode)
|
||||
}
|
||||
// BuildOptions here is nested inside CreateOptions, so
|
||||
// no service list is passed, it will implicitly pick all
|
||||
// services being created, which includes any explicitly
|
||||
// specified via "services" arg here as well as deps
|
||||
bo, err := buildOptions.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bo.Services = services
|
||||
bo.Deps = !upOptions.noDeps
|
||||
build = &bo
|
||||
}
|
||||
|
||||
create := api.CreateOptions{
|
||||
Build: build,
|
||||
Services: services,
|
||||
RemoveOrphans: createOptions.removeOrphans,
|
||||
IgnoreOrphans: createOptions.ignoreOrphans,
|
||||
Recreate: createOptions.recreateStrategy(),
|
||||
RecreateDependencies: createOptions.dependenciesRecreateStrategy(),
|
||||
Inherit: !createOptions.noInherit,
|
||||
Timeout: createOptions.GetTimeout(),
|
||||
QuietPull: createOptions.quietPull,
|
||||
AssumeYes: createOptions.AssumeYes,
|
||||
}
|
||||
|
||||
if upOptions.noStart {
|
||||
return backend.Create(ctx, project, create)
|
||||
}
|
||||
|
||||
var consumer api.LogConsumer
|
||||
var attach []string
|
||||
if !upOptions.Detach {
|
||||
consumer = formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
|
||||
|
||||
var attachSet utils.Set[string]
|
||||
if len(upOptions.attach) != 0 {
|
||||
// services are passed explicitly with --attach, verify they're valid and then use them as-is
|
||||
attachSet = utils.NewSet(upOptions.attach...)
|
||||
unexpectedSvcs := attachSet.Diff(utils.NewSet(project.ServiceNames()...))
|
||||
if len(unexpectedSvcs) != 0 {
|
||||
return fmt.Errorf("cannot attach to services not included in up: %s", strings.Join(unexpectedSvcs.Elements(), ", "))
|
||||
}
|
||||
} else {
|
||||
// mark services being launched (and potentially their deps) for attach
|
||||
// if they didn't opt-out via Compose YAML
|
||||
attachSet = utils.NewSet[string]()
|
||||
var dependencyOpt types.DependencyOption = types.IgnoreDependencies
|
||||
if upOptions.attachDependencies {
|
||||
dependencyOpt = types.IncludeDependencies
|
||||
}
|
||||
if err := project.ForEachService(services, func(serviceName string, s *types.ServiceConfig) error {
|
||||
if s.Attach == nil || *s.Attach {
|
||||
attachSet.Add(serviceName)
|
||||
}
|
||||
return nil
|
||||
}, dependencyOpt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// filter out any services that have been explicitly marked for ignore with `--no-attach`
|
||||
attachSet.RemoveAll(upOptions.noAttach...)
|
||||
attach = attachSet.Elements()
|
||||
}
|
||||
|
||||
timeout := time.Duration(upOptions.waitTimeout) * time.Second
|
||||
return backend.Up(ctx, project, api.UpOptions{
|
||||
Create: create,
|
||||
Start: api.StartOptions{
|
||||
Project: project,
|
||||
Attach: consumer,
|
||||
AttachTo: attach,
|
||||
ExitCodeFrom: upOptions.exitCodeFrom,
|
||||
OnExit: upOptions.OnExit(),
|
||||
Wait: upOptions.wait,
|
||||
WaitTimeout: timeout,
|
||||
Watch: upOptions.watch,
|
||||
Services: services,
|
||||
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func setServiceScale(project *types.Project, name string, replicas int) error {
|
||||
service, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.SetScale(replicas)
|
||||
project.Services[name] = service
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestApplyScaleOpt(t *testing.T) {
|
||||
p := types.Project{
|
||||
Services: types.Services{
|
||||
"foo": {
|
||||
Name: "foo",
|
||||
},
|
||||
"bar": {
|
||||
Name: "bar",
|
||||
Deploy: &types.DeployConfig{
|
||||
Mode: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := applyScaleOpts(&p, []string{"foo=2", "bar=3"})
|
||||
assert.NilError(t, err)
|
||||
foo, err := p.GetService("foo")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, *foo.Scale, 2)
|
||||
|
||||
bar, err := p.GetService("bar")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, *bar.Scale, 3)
|
||||
assert.Equal(t, *bar.Deploy.Replicas, 3)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
)
|
||||
|
||||
type versionOptions struct {
|
||||
format string
|
||||
short bool
|
||||
}
|
||||
|
||||
func versionCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := versionOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "version [OPTIONS]",
|
||||
Short: "Show the Docker Compose version information",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
runVersion(opts, dockerCli)
|
||||
return nil
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// overwrite parent PersistentPreRunE to avoid trying to load
|
||||
// compose file on version command if COMPOSE_FILE is set
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// define flags for backward compatibility with com.docker.cli
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
|
||||
flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runVersion(opts versionOptions, dockerCli command.Cli) {
|
||||
if opts.short {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), strings.TrimPrefix(internal.Version, "v"))
|
||||
return
|
||||
}
|
||||
if opts.format == formatter.JSON {
|
||||
_, _ = fmt.Fprintf(dockerCli.Out(), "{\"version\":%q}\n", internal.Version)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), "Docker Compose version", internal.Version)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2025 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 (
|
||||
"bytes"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
originalVersion := internal.Version
|
||||
defer func() {
|
||||
internal.Version = originalVersion
|
||||
}()
|
||||
internal.Version = "v9.9.9-test"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: []string{},
|
||||
want: "Docker Compose version v9.9.9-test\n",
|
||||
},
|
||||
{
|
||||
name: "short flag",
|
||||
args: []string{"--short"},
|
||||
want: "9.9.9-test\n",
|
||||
},
|
||||
{
|
||||
name: "json flag",
|
||||
args: []string{"--format", "json"},
|
||||
want: `{"version":"v9.9.9-test"}` + "\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
|
||||
|
||||
cmd := versionCommand(cli)
|
||||
cmd.SetArgs(test.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, test.want, buf.String())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type vizOptions struct {
|
||||
*ProjectOptions
|
||||
includeNetworks bool
|
||||
includePorts bool
|
||||
includeImageName bool
|
||||
indentationStr string
|
||||
}
|
||||
|
||||
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := vizOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
var indentationSize int
|
||||
var useSpaces bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "viz [OPTIONS]",
|
||||
Short: "EXPERIMENTAL - Generate a graphviz graph from your compose file",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
var err error
|
||||
opts.indentationStr, err = preferredIndentationStr(indentationSize, useSpaces)
|
||||
return err
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runViz(ctx, dockerCli, backend, &opts)
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.includePorts, "ports", false, "Include service's exposed ports in output graph")
|
||||
cmd.Flags().BoolVar(&opts.includeNetworks, "networks", false, "Include service's attached networks in output graph")
|
||||
cmd.Flags().BoolVar(&opts.includeImageName, "image", false, "Include service's image name in output graph")
|
||||
cmd.Flags().IntVar(&indentationSize, "indentation-size", 1, "Number of tabs or spaces to use for indentation")
|
||||
cmd.Flags().BoolVar(&useSpaces, "spaces", false, "If given, space character ' ' will be used to indent,\notherwise tab character '\\t' will be used")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// build graph
|
||||
graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
|
||||
IncludeNetworks: opts.includeNetworks,
|
||||
IncludePorts: opts.includePorts,
|
||||
IncludeImageName: opts.includeImageName,
|
||||
Indentation: opts.indentationStr,
|
||||
})
|
||||
|
||||
fmt.Println(graphStr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preferredIndentationStr returns a single string given the indentation preference
|
||||
func preferredIndentationStr(size int, useSpace bool) (string, error) {
|
||||
if size < 0 {
|
||||
return "", fmt.Errorf("invalid indentation size: %d", size)
|
||||
}
|
||||
|
||||
indentationStr := "\t"
|
||||
if useSpace {
|
||||
indentationStr = " "
|
||||
}
|
||||
return strings.Repeat(indentationStr, size), nil
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPreferredIndentationStr(t *testing.T) {
|
||||
type args struct {
|
||||
size int
|
||||
useSpace bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should return '\\t\\t'",
|
||||
args: args{
|
||||
size: 2,
|
||||
useSpace: false,
|
||||
},
|
||||
want: "\t\t",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should return ' '",
|
||||
args: args{
|
||||
size: 4,
|
||||
useSpace: true,
|
||||
},
|
||||
want: " ",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should return ''",
|
||||
args: args{
|
||||
size: 0,
|
||||
useSpace: false,
|
||||
},
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should return ''",
|
||||
args: args{
|
||||
size: 0,
|
||||
useSpace: true,
|
||||
},
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should throw error because indentation size < 0",
|
||||
args: args{
|
||||
size: -1,
|
||||
useSpace: false,
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace)
|
||||
if tt.wantErr {
|
||||
require.Errorf(t, err, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type volumesOptions struct {
|
||||
*ProjectOptions
|
||||
Quiet bool
|
||||
Format string
|
||||
}
|
||||
|
||||
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := volumesOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "volumes [OPTIONS] [SERVICE...]",
|
||||
Short: "List volumes",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runVol(ctx, dockerCli, backend, args, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
|
||||
cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
|
||||
|
||||
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...)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
volumes, err := backend.Volumes(ctx, project, api.VolumesOptions{
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Quiet {
|
||||
for _, v := range volumes {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
volumeCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
|
||||
}
|
||||
|
||||
return formatter.VolumeWrite(volumeCtx, volumes)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type waitOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
services []string
|
||||
|
||||
downProject bool
|
||||
}
|
||||
|
||||
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := waitOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
var statusCode int64
|
||||
var err error
|
||||
cmd := &cobra.Command{
|
||||
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
|
||||
Short: "Block until containers of all (or specified) services stop.",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: Adapt(func(ctx context.Context, services []string) error {
|
||||
opts.services = services
|
||||
statusCode, err = runWait(ctx, dockerCli, backend, &opts)
|
||||
return err
|
||||
}),
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
os.Exit(int(statusCode))
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
|
||||
_, name, err := opts.projectOrName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return backend.Wait(ctx, name, api.WaitOptions{
|
||||
Services: opts.services,
|
||||
DownProjectOnContainerExit: opts.downProject,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
type watchOptions struct {
|
||||
*ProjectOptions
|
||||
prune bool
|
||||
noUp bool
|
||||
}
|
||||
|
||||
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
watchOpts := watchOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "watch [SERVICE...]",
|
||||
Short: "Watch build context for service and rebuild/refresh containers when files are updated",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}),
|
||||
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
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)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output")
|
||||
cmd.Flags().BoolVar(&watchOpts.prune, "prune", true, "Prune dangling images on rebuild")
|
||||
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
build, err := buildOpts.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// validation done -- ensure we have the lockfile for this project before doing work
|
||||
l, err := locker.NewPidfile(project.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
|
||||
}
|
||||
if err := l.Lock(); err != nil {
|
||||
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
|
||||
}
|
||||
|
||||
if !watchOpts.noUp {
|
||||
for index, service := range project.Services {
|
||||
if service.Build != nil && service.Develop != nil {
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
}
|
||||
project.Services[index] = service
|
||||
}
|
||||
upOpts := api.UpOptions{
|
||||
Create: api.CreateOptions{
|
||||
Build: &build,
|
||||
Services: services,
|
||||
RemoveOrphans: false,
|
||||
Recreate: api.RecreateDiverged,
|
||||
RecreateDependencies: api.RecreateNever,
|
||||
Inherit: true,
|
||||
QuietPull: buildOpts.quiet,
|
||||
},
|
||||
Start: api.StartOptions{
|
||||
Project: project,
|
||||
Attach: nil,
|
||||
Services: services,
|
||||
},
|
||||
}
|
||||
if err := backend.Up(ctx, project, upOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
|
||||
return backend.Watch(ctx, project, api.WatchOptions{
|
||||
Build: &build,
|
||||
LogTo: consumer,
|
||||
Prune: watchOpts.prune,
|
||||
Services: services,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
)
|
||||
|
||||
var disableAnsi bool
|
||||
|
||||
func ansi(code string) string {
|
||||
return fmt.Sprintf("\033%s", code)
|
||||
}
|
||||
|
||||
func saveCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("7"))
|
||||
}
|
||||
|
||||
func restoreCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("8"))
|
||||
}
|
||||
|
||||
func showCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("[?25h"))
|
||||
}
|
||||
|
||||
func moveCursor(y, x int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
|
||||
}
|
||||
|
||||
func carriageReturn() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dG", 0)))
|
||||
}
|
||||
|
||||
func clearLine() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not move cursor from its current position
|
||||
fmt.Print(ansi("[2K"))
|
||||
}
|
||||
|
||||
func moveCursorUp(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not add new lines
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
|
||||
}
|
||||
|
||||
func moveCursorDown(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
// Does not add new lines
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
|
||||
}
|
||||
|
||||
func newLine() {
|
||||
// Like \n
|
||||
fmt.Print("\012")
|
||||
}
|
||||
|
||||
func lenAnsi(s string) int {
|
||||
// len has into consideration ansi codes, if we want
|
||||
// the len of the actual len(string) we need to strip
|
||||
// all ansi codes
|
||||
return len(stripansi.Strip(s))
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
)
|
||||
|
||||
var names = []string{
|
||||
"grey",
|
||||
"red",
|
||||
"green",
|
||||
"yellow",
|
||||
"blue",
|
||||
"magenta",
|
||||
"cyan",
|
||||
"white",
|
||||
}
|
||||
|
||||
const (
|
||||
BOLD = "1"
|
||||
FAINT = "2"
|
||||
ITALIC = "3"
|
||||
UNDERLINE = "4"
|
||||
)
|
||||
|
||||
const (
|
||||
RESET = "0"
|
||||
CYAN = "36"
|
||||
)
|
||||
|
||||
const (
|
||||
// Never use ANSI codes
|
||||
Never = "never"
|
||||
|
||||
// Always use ANSI codes
|
||||
Always = "always"
|
||||
|
||||
// Auto detect terminal is a tty and can use ANSI codes
|
||||
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) {
|
||||
nextColor = func() colorFunc {
|
||||
return monochrome
|
||||
}
|
||||
disableAnsi = true
|
||||
}
|
||||
}
|
||||
|
||||
func useAnsi(streams command.Streams, ansi string) bool {
|
||||
switch ansi {
|
||||
case Always:
|
||||
return true
|
||||
case Auto:
|
||||
return streams.Out().IsTerminal()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// colorFunc use ANSI codes to render colored text on console
|
||||
type colorFunc func(s string) string
|
||||
|
||||
var monochrome = func(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func ansiColor(code, s string, formatOpts ...string) string {
|
||||
return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0"))
|
||||
}
|
||||
|
||||
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
|
||||
func ansiColorCode(code string, formatOpts ...string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\033[")
|
||||
for _, c := range formatOpts {
|
||||
sb.WriteString(c)
|
||||
sb.WriteString(";")
|
||||
}
|
||||
sb.WriteString(code)
|
||||
sb.WriteString("m")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func makeColorFunc(code string) colorFunc {
|
||||
return func(s string) string {
|
||||
return ansiColor(code, s)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
nextColor = rainbowColor
|
||||
rainbow []colorFunc
|
||||
currentIndex = 0
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
func rainbowColor() colorFunc {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
result := rainbow[currentIndex]
|
||||
currentIndex = (currentIndex + 1) % len(rainbow)
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
colors := map[string]colorFunc{}
|
||||
for i, name := range names {
|
||||
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
|
||||
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
|
||||
}
|
||||
rainbow = []colorFunc{
|
||||
colors["cyan"],
|
||||
colors["yellow"],
|
||||
colors["green"],
|
||||
colors["magenta"],
|
||||
colors["blue"],
|
||||
colors["intense_cyan"],
|
||||
colors["intense_yellow"],
|
||||
colors["intense_green"],
|
||||
colors["intense_magenta"],
|
||||
colors["intense_blue"],
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
const (
|
||||
// JSON Print in JSON format
|
||||
JSON = "json"
|
||||
// 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)
|
||||
TABLE = "table"
|
||||
)
|
|
@ -0,0 +1,287 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
nameHeader = "NAME"
|
||||
projectHeader = "PROJECT"
|
||||
serviceHeader = "SERVICE"
|
||||
commandHeader = "COMMAND"
|
||||
runningForHeader = "CREATED"
|
||||
mountsHeader = "MOUNTS"
|
||||
localVolumes = "LOCAL VOLUMES"
|
||||
networksHeader = "NETWORKS"
|
||||
)
|
||||
|
||||
// NewContainerFormat returns a Format for rendering using a Context
|
||||
func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
|
||||
switch source {
|
||||
case formatter.TableFormatKey, "": // table formatting is the default if none is set.
|
||||
if quiet {
|
||||
return formatter.DefaultQuietFormat
|
||||
}
|
||||
format := defaultContainerTableFormat
|
||||
if size {
|
||||
format += `\t{{.Size}}`
|
||||
}
|
||||
return formatter.Format(format)
|
||||
case formatter.RawFormatKey:
|
||||
if quiet {
|
||||
return `container_id: {{.ID}}`
|
||||
}
|
||||
format := `container_id: {{.ID}}
|
||||
image: {{.Image}}
|
||||
command: {{.Command}}
|
||||
created_at: {{.CreatedAt}}
|
||||
state: {{- pad .State 1 0}}
|
||||
status: {{- pad .Status 1 0}}
|
||||
names: {{.Names}}
|
||||
labels: {{- pad .Labels 1 0}}
|
||||
ports: {{- pad .Ports 1 0}}
|
||||
`
|
||||
if size {
|
||||
format += `size: {{.Size}}\n`
|
||||
}
|
||||
return formatter.Format(format)
|
||||
default: // custom format
|
||||
if quiet {
|
||||
return formatter.DefaultQuietFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerWrite renders the context for a list of containers
|
||||
func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, container := range containers {
|
||||
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(NewContainerContext(), render)
|
||||
}
|
||||
|
||||
// ContainerContext is a struct used for rendering a list of containers in a Go template.
|
||||
type ContainerContext struct {
|
||||
formatter.HeaderContext
|
||||
trunc bool
|
||||
c api.ContainerSummary
|
||||
|
||||
// FieldsUsed is used in the pre-processing step to detect which fields are
|
||||
// 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{}
|
||||
}
|
||||
|
||||
// NewContainerContext creates a new context for rendering containers
|
||||
func NewContainerContext() *ContainerContext {
|
||||
containerCtx := ContainerContext{}
|
||||
containerCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": formatter.ContainerIDHeader,
|
||||
"Name": nameHeader,
|
||||
"Project": projectHeader,
|
||||
"Service": serviceHeader,
|
||||
"Image": formatter.ImageHeader,
|
||||
"Command": commandHeader,
|
||||
"CreatedAt": formatter.CreatedAtHeader,
|
||||
"RunningFor": runningForHeader,
|
||||
"Ports": formatter.PortsHeader,
|
||||
"State": formatter.StateHeader,
|
||||
"Status": formatter.StatusHeader,
|
||||
"Size": formatter.SizeHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return &containerCtx
|
||||
}
|
||||
|
||||
// MarshalJSON makes ContainerContext implement json.Marshaler
|
||||
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
// ID returns the container's ID as a string. Depending on the `--no-trunc`
|
||||
// option being set, the full or truncated ID is returned.
|
||||
func (c *ContainerContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.c.ID)
|
||||
}
|
||||
return c.c.ID
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Name() string {
|
||||
return c.c.Name
|
||||
}
|
||||
|
||||
// Names returns a comma-separated string of the container's names, with their
|
||||
// slash (/) prefix stripped. Additional names for the container (related to the
|
||||
// legacy `--link` feature) are omitted.
|
||||
func (c *ContainerContext) Names() string {
|
||||
names := formatter.StripNamePrefix(c.c.Names)
|
||||
if c.trunc {
|
||||
for _, name := range names {
|
||||
if len(strings.Split(name, "/")) == 1 {
|
||||
names = []string{name}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Service() string {
|
||||
return c.c.Service
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Project() string {
|
||||
return c.c.Project
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Image() string {
|
||||
return c.c.Image
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Command() string {
|
||||
command := c.c.Command
|
||||
if c.trunc {
|
||||
command = formatter.Ellipsis(command, 20)
|
||||
}
|
||||
return strconv.Quote(command)
|
||||
}
|
||||
|
||||
func (c *ContainerContext) CreatedAt() string {
|
||||
return time.Unix(c.c.Created, 0).String()
|
||||
}
|
||||
|
||||
func (c *ContainerContext) RunningFor() string {
|
||||
createdAt := time.Unix(c.c.Created, 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *ContainerContext) ExitCode() int {
|
||||
return c.c.ExitCode
|
||||
}
|
||||
|
||||
func (c *ContainerContext) State() string {
|
||||
return c.c.State
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Status() string {
|
||||
return c.c.Status
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Health() string {
|
||||
return c.c.Health
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Publishers() api.PortPublishers {
|
||||
return c.c.Publishers
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Ports() string {
|
||||
var ports []container.Port
|
||||
for _, publisher := range c.c.Publishers {
|
||||
ports = append(ports, container.Port{
|
||||
IP: publisher.URL,
|
||||
PrivatePort: uint16(publisher.TargetPort),
|
||||
PublicPort: uint16(publisher.PublishedPort),
|
||||
Type: publisher.Protocol,
|
||||
})
|
||||
}
|
||||
return formatter.DisplayablePorts(ports)
|
||||
}
|
||||
|
||||
// Labels returns a comma-separated string of labels present on the container.
|
||||
func (c *ContainerContext) Labels() string {
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var joinLabels []string
|
||||
for k, v := range c.c.Labels {
|
||||
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
// Label returns the value of the label with the given name or an empty string
|
||||
// if the given label does not exist.
|
||||
func (c *ContainerContext) Label(name string) string {
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
return c.c.Labels[name]
|
||||
}
|
||||
|
||||
// Mounts returns a comma-separated string of mount names present on the container.
|
||||
// If the trunc option is set, names can be truncated (ellipsized).
|
||||
func (c *ContainerContext) Mounts() string {
|
||||
var mounts []string
|
||||
for _, name := range c.c.Mounts {
|
||||
if c.trunc {
|
||||
name = formatter.Ellipsis(name, 15)
|
||||
}
|
||||
mounts = append(mounts, name)
|
||||
}
|
||||
return strings.Join(mounts, ",")
|
||||
}
|
||||
|
||||
// LocalVolumes returns the number of volumes using the "local" volume driver.
|
||||
func (c *ContainerContext) LocalVolumes() string {
|
||||
return fmt.Sprintf("%d", c.c.LocalVolumes)
|
||||
}
|
||||
|
||||
// Networks returns a comma-separated string of networks that the container is
|
||||
// attached to.
|
||||
func (c *ContainerContext) Networks() string {
|
||||
return strings.Join(c.c.Networks, ",")
|
||||
}
|
||||
|
||||
// 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["Size"] = struct{}{}
|
||||
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
||||
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
|
||||
|
||||
sf := srw
|
||||
if c.c.SizeRootFs > 0 {
|
||||
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
||||
}
|
||||
return sf
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/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 {
|
||||
switch strings.ToLower(format) {
|
||||
case TABLE, PRETTY, "":
|
||||
return PrintPrettySection(outWriter, writerFn, headers...)
|
||||
case TemplateLegacyJSON:
|
||||
switch reflect.TypeOf(toJSON).Kind() {
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(toJSON)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
obj := s.Index(i).Interface()
|
||||
outJSON, err := ToJSON(obj, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprint(outWriter, outJSON)
|
||||
}
|
||||
default:
|
||||
outJSON, err := ToStandardJSON(toJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(outWriter, outJSON)
|
||||
}
|
||||
case JSON:
|
||||
switch reflect.TypeOf(toJSON).Kind() {
|
||||
case reflect.Slice:
|
||||
outJSON, err := ToJSON(toJSON, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprint(outWriter, outJSON)
|
||||
default:
|
||||
outJSON, err := ToStandardJSON(toJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(outWriter, outJSON)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("format value %q could not be parsed: %w", format, api.ErrParsingFailed)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/goleak"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type testStruct struct {
|
||||
Name string
|
||||
Status string
|
||||
}
|
||||
|
||||
// Print prints formatted lists in different formats
|
||||
func TestPrint(t *testing.T) {
|
||||
testList := []testStruct{
|
||||
{
|
||||
Name: "myName1",
|
||||
Status: "myStatus1",
|
||||
},
|
||||
{
|
||||
Name: "myName2",
|
||||
Status: "myStatus2",
|
||||
},
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) {
|
||||
for _, t := range testList {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
|
||||
}
|
||||
}, "NAME", "STATUS"))
|
||||
assert.Equal(t, b.String(), "NAME STATUS\nmyName1 myStatus1\nmyName2 myStatus2\n")
|
||||
|
||||
b.Reset()
|
||||
assert.NilError(t, Print(testList, JSON, b, func(w io.Writer) {
|
||||
for _, t := range testList {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
|
||||
}
|
||||
}, "NAME", "STATUS"))
|
||||
assert.Equal(t, b.String(), `[{"Name":"myName1","Status":"myStatus1"},{"Name":"myName2","Status":"myStatus2"}]
|
||||
`)
|
||||
|
||||
b.Reset()
|
||||
assert.NilError(t, Print(testList, TemplateLegacyJSON, b, func(w io.Writer) {
|
||||
for _, t := range testList {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
|
||||
}
|
||||
}, "NAME", "STATUS"))
|
||||
json := b.String()
|
||||
assert.Equal(t, json, `{"Name":"myName1","Status":"myStatus1"}
|
||||
{"Name":"myName2","Status":"myStatus2"}
|
||||
`)
|
||||
}
|
||||
|
||||
func TestColorsGoroutinesLeak(t *testing.T) {
|
||||
goleak.VerifyNone(t)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const standardIndentation = " "
|
||||
|
||||
// ToStandardJSON return a string with the JSON representation of the interface{}
|
||||
func ToStandardJSON(i interface{}) (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) {
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent(prefix, indentation)
|
||||
err := encoder.Encode(i)
|
||||
return buffer.String(), err
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
)
|
||||
|
||||
// LogConsumer consume logs from services and format them
|
||||
type logConsumer struct {
|
||||
ctx context.Context
|
||||
presenters sync.Map // map[string]*presenter
|
||||
width int
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
color bool
|
||||
prefix bool
|
||||
timestamp bool
|
||||
}
|
||||
|
||||
// NewLogConsumer creates a new LogConsumer
|
||||
func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix, timestamp bool) api.LogConsumer {
|
||||
return &logConsumer{
|
||||
ctx: ctx,
|
||||
presenters: sync.Map{},
|
||||
width: 0,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
color: color,
|
||||
prefix: prefix,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) register(name string) *presenter {
|
||||
var p *presenter
|
||||
root, _, found := strings.Cut(name, " ")
|
||||
if found {
|
||||
parent := l.getPresenter(root)
|
||||
p = &presenter{
|
||||
colors: parent.colors,
|
||||
name: name,
|
||||
prefix: parent.prefix,
|
||||
}
|
||||
} else {
|
||||
cf := monochrome
|
||||
if l.color {
|
||||
switch name {
|
||||
case "":
|
||||
cf = monochrome
|
||||
case api.WatchLogger:
|
||||
cf = makeColorFunc("92")
|
||||
default:
|
||||
cf = nextColor()
|
||||
}
|
||||
}
|
||||
p = &presenter{
|
||||
colors: cf,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
l.presenters.Store(name, p)
|
||||
l.computeWidth()
|
||||
if l.prefix {
|
||||
l.presenters.Range(func(key, value interface{}) bool {
|
||||
p := value.(*presenter)
|
||||
p.setPrefix(l.width)
|
||||
return true
|
||||
})
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (l *logConsumer) getPresenter(container string) *presenter {
|
||||
p, ok := l.presenters.Load(container)
|
||||
if !ok { // should have been registered, but ¯\_(ツ)_/¯
|
||||
return l.register(container)
|
||||
}
|
||||
return p.(*presenter)
|
||||
}
|
||||
|
||||
// Log formats a log message as received from name/container
|
||||
func (l *logConsumer) Log(container, message string) {
|
||||
l.write(l.stdout, container, message)
|
||||
}
|
||||
|
||||
// Err formats a log message as received from name/container
|
||||
func (l *logConsumer) Err(container, message string) {
|
||||
l.write(l.stderr, container, message)
|
||||
}
|
||||
|
||||
func (l *logConsumer) write(w io.Writer, container, message string) {
|
||||
if l.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
p := l.getPresenter(container)
|
||||
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
if l.timestamp {
|
||||
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Status(container, msg string) {
|
||||
p := l.getPresenter(container)
|
||||
s := p.colors(fmt.Sprintf("%s%s %s\n", goterm.RESET_LINE, container, msg))
|
||||
l.stdout.Write([]byte(s)) //nolint:errcheck
|
||||
}
|
||||
|
||||
func (l *logConsumer) computeWidth() {
|
||||
width := 0
|
||||
l.presenters.Range(func(key, value interface{}) bool {
|
||||
p := value.(*presenter)
|
||||
if len(p.name) > width {
|
||||
width = len(p.name)
|
||||
}
|
||||
return true
|
||||
})
|
||||
l.width = width + 1
|
||||
}
|
||||
|
||||
type presenter struct {
|
||||
colors colorFunc
|
||||
name string
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (p *presenter) setPrefix(width int) {
|
||||
if p.name == api.WatchLogger {
|
||||
p.prefix = p.colors(strings.Repeat(" ", width) + " ⦿ ")
|
||||
return
|
||||
}
|
||||
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
|
||||
}
|
||||
|
||||
type logDecorator struct {
|
||||
decorated api.LogConsumer
|
||||
Before func()
|
||||
After func()
|
||||
}
|
||||
|
||||
func (l logDecorator) Log(containerName, message string) {
|
||||
l.Before()
|
||||
l.decorated.Log(containerName, message)
|
||||
l.After()
|
||||
}
|
||||
|
||||
func (l logDecorator) Err(containerName, message string) {
|
||||
l.Before()
|
||||
l.decorated.Err(containerName, message)
|
||||
l.After()
|
||||
}
|
||||
|
||||
func (l logDecorator) Status(container, msg string) {
|
||||
l.Before()
|
||||
l.decorated.Status(container, msg)
|
||||
l.After()
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
// SetMultiErrorFormat set cli default format for multi-errors
|
||||
func SetMultiErrorFormat(errs *multierror.Error) {
|
||||
if errs != nil {
|
||||
errs.ErrorFormat = formatErrors
|
||||
}
|
||||
}
|
||||
|
||||
func formatErrors(errs []error) string {
|
||||
messages := make([]string, len(errs))
|
||||
for i, err := range errs {
|
||||
messages[i] = "Error: " + err.Error()
|
||||
}
|
||||
return strings.Join(messages, "\n")
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// PrintPrettySection prints a tabbed section on the writer parameter
|
||||
func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error {
|
||||
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
|
||||
printer(w)
|
||||
return w.Flush()
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
const DISPLAY_ERROR_TIME = 10
|
||||
|
||||
type KeyboardError struct {
|
||||
err error
|
||||
timeStart time.Time
|
||||
}
|
||||
|
||||
func (ke *KeyboardError) shouldDisplay() bool {
|
||||
return ke.err != nil && int(time.Since(ke.timeStart).Seconds()) < DISPLAY_ERROR_TIME
|
||||
}
|
||||
|
||||
func (ke *KeyboardError) printError(height int, info string) {
|
||||
if ke.shouldDisplay() {
|
||||
errMessage := ke.err.Error()
|
||||
|
||||
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
||||
clearLine()
|
||||
|
||||
fmt.Print(errMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (ke *KeyboardError) addError(prefix string, err error) {
|
||||
ke.timeStart = time.Now()
|
||||
|
||||
prefix = ansiColor(CYAN, fmt.Sprintf("%s →", prefix), BOLD)
|
||||
errorString := fmt.Sprintf("%s %s", prefix, err.Error())
|
||||
|
||||
ke.err = errors.New(errorString)
|
||||
}
|
||||
|
||||
func (ke *KeyboardError) error() string {
|
||||
return ke.err.Error()
|
||||
}
|
||||
|
||||
type KeyboardWatch struct {
|
||||
Watching bool
|
||||
Watcher Feature
|
||||
}
|
||||
|
||||
// Feature is an compose feature that can be started/stopped by a menu command
|
||||
type Feature interface {
|
||||
Start(context.Context) error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type KEYBOARD_LOG_LEVEL int
|
||||
|
||||
const (
|
||||
NONE KEYBOARD_LOG_LEVEL = 0
|
||||
INFO KEYBOARD_LOG_LEVEL = 1
|
||||
DEBUG KEYBOARD_LOG_LEVEL = 2
|
||||
)
|
||||
|
||||
type LogKeyboard struct {
|
||||
kError KeyboardError
|
||||
Watch *KeyboardWatch
|
||||
IsDockerDesktopActive bool
|
||||
logLevel KEYBOARD_LOG_LEVEL
|
||||
signalChannel chan<- os.Signal
|
||||
}
|
||||
|
||||
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
|
||||
return &LogKeyboard{
|
||||
IsDockerDesktopActive: isDockerDesktopActive,
|
||||
logLevel: INFO,
|
||||
signalChannel: sc,
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) Decorate(l api.LogConsumer) api.LogConsumer {
|
||||
return logDecorator{
|
||||
decorated: l,
|
||||
Before: lk.clearNavigationMenu,
|
||||
After: lk.PrintKeyboardInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) PrintKeyboardInfo() {
|
||||
if lk.logLevel == INFO {
|
||||
lk.printNavigationMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Creates space to print error and menu string
|
||||
func (lk *LogKeyboard) createBuffer(lines int) {
|
||||
if lk.kError.shouldDisplay() {
|
||||
extraLines := extraLines(lk.kError.error()) + 1
|
||||
lines += extraLines
|
||||
}
|
||||
|
||||
// get the string
|
||||
infoMessage := lk.navigationMenu()
|
||||
// calculate how many lines we need to display the menu info
|
||||
// might be needed a line break
|
||||
extraLines := extraLines(infoMessage) + 1
|
||||
lines += extraLines
|
||||
|
||||
if lines > 0 {
|
||||
allocateSpace(lines)
|
||||
moveCursorUp(lines)
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) printNavigationMenu() {
|
||||
offset := 1
|
||||
lk.clearNavigationMenu()
|
||||
lk.createBuffer(offset)
|
||||
|
||||
if lk.logLevel == INFO {
|
||||
height := goterm.Height()
|
||||
menu := lk.navigationMenu()
|
||||
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
lk.kError.printError(height, menu)
|
||||
|
||||
moveCursor(height-extraLines(menu), 0)
|
||||
clearLine()
|
||||
fmt.Print(menu)
|
||||
|
||||
carriageReturn()
|
||||
restoreCursor()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) navigationMenu() string {
|
||||
var openDDInfo string
|
||||
if lk.IsDockerDesktopActive {
|
||||
openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
|
||||
}
|
||||
|
||||
var openDDUI string
|
||||
if openDDInfo != "" {
|
||||
openDDUI = navColor(" ")
|
||||
}
|
||||
if lk.IsDockerDesktopActive {
|
||||
openDDUI = openDDUI + 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
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) clearNavigationMenu() {
|
||||
height := goterm.Height()
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
// clearLine()
|
||||
for i := 0; i < height; i++ {
|
||||
moveCursorDown(1)
|
||||
clearLine()
|
||||
}
|
||||
restoreCursor()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
||||
if !lk.IsDockerDesktopActive {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop")
|
||||
lk.keyboardError("View", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
|
||||
if !lk.IsDockerDesktopActive {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("View Config", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("Watch Docs", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
||||
lk.kError.addError(prefix, err)
|
||||
|
||||
lk.printNavigationMenu()
|
||||
timer1 := time.NewTimer((DISPLAY_ERROR_TIME + 1) * time.Second)
|
||||
go func() {
|
||||
<-timer1.C
|
||||
lk.printNavigationMenu()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
|
||||
if lk.Watch == nil {
|
||||
return
|
||||
}
|
||||
if lk.Watch.Watching {
|
||||
err := lk.Watch.Watcher.Stop()
|
||||
if err != nil {
|
||||
options.Start.Attach.Err(api.WatchLogger, err.Error())
|
||||
} else {
|
||||
lk.Watch.Watching = false
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := lk.Watch.Watcher.Start(ctx)
|
||||
if err != nil {
|
||||
options.Start.Attach.Err(api.WatchLogger, err.Error())
|
||||
} else {
|
||||
lk.Watch.Watching = true
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
|
||||
switch kRune := event.Rune; kRune {
|
||||
case 'v':
|
||||
lk.openDockerDesktop(ctx, project)
|
||||
case 'w':
|
||||
if lk.Watch == nil {
|
||||
// we try to open watch docs if DD is installed
|
||||
if lk.IsDockerDesktopActive {
|
||||
lk.openDDWatchDocs(ctx, project)
|
||||
}
|
||||
// either way we mark menu/watch as an error
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
||||
lk.keyboardError("Watch", err)
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
lk.ToggleWatch(ctx, options)
|
||||
case 'o':
|
||||
lk.openDDComposeUI(ctx, project)
|
||||
}
|
||||
switch key := event.Key; key {
|
||||
case keyboard.KeyCtrlC:
|
||||
_ = keyboard.Close()
|
||||
lk.clearNavigationMenu()
|
||||
showCursor()
|
||||
|
||||
lk.logLevel = NONE
|
||||
// will notify main thread to kill and will handle gracefully
|
||||
lk.signalChannel <- syscall.SIGINT
|
||||
case keyboard.KeyEnter:
|
||||
newLine()
|
||||
lk.printNavigationMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
|
||||
lk.Watch = &KeyboardWatch{
|
||||
Watching: enabled,
|
||||
Watcher: watcher,
|
||||
}
|
||||
}
|
||||
|
||||
func allocateSpace(lines int) {
|
||||
for i := 0; i < lines; i++ {
|
||||
clearLine()
|
||||
newLine()
|
||||
carriageReturn()
|
||||
}
|
||||
}
|
||||
|
||||
func extraLines(s string) int {
|
||||
return int(math.Floor(float64(lenAnsi(s)) / float64(goterm.Width())))
|
||||
}
|
||||
|
||||
func shortcutKeyColor(key string) string {
|
||||
foreground := "38;2"
|
||||
black := "0;0;0"
|
||||
background := "48;2"
|
||||
white := "255;255;255"
|
||||
return ansiColor(foreground+";"+black+";"+background+";"+white, key, BOLD)
|
||||
}
|
||||
|
||||
func navColor(key string) string {
|
||||
return ansiColor(FAINT, key)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if originalPreRunE != nil {
|
||||
return originalPreRunE(cmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: 1,
|
||||
Status: err.Error(),
|
||||
}
|
||||
})
|
||||
return cmd
|
||||
},
|
||||
manager.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
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
if plugin.RunningStandalone() {
|
||||
os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
|
||||
}
|
||||
pluginMain()
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
target: auto
|
||||
threshold: 2%
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
comment:
|
||||
require_changes: true
|
||||
|
||||
ignore:
|
||||
- "packaging"
|
||||
- "docs"
|
||||
- "bin"
|
||||
- "e2e"
|
||||
- "pkg/e2e"
|
||||
- "**/*_test.go"
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright 2022 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.
|
||||
|
||||
variable "GO_VERSION" {
|
||||
# default ARG value set in Dockerfile
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "BUILD_TAGS" {
|
||||
default = "e2e"
|
||||
}
|
||||
|
||||
variable "DOCS_FORMATS" {
|
||||
default = "md,yaml"
|
||||
}
|
||||
|
||||
# Defines the output folder to override the default behavior.
|
||||
# See Makefile for details, this is generally only useful for
|
||||
# the packaging scripts and care should be taken to not break
|
||||
# them.
|
||||
variable "DESTDIR" {
|
||||
default = ""
|
||||
}
|
||||
function "outdir" {
|
||||
params = [defaultdir]
|
||||
result = DESTDIR != "" ? DESTDIR : "${defaultdir}"
|
||||
}
|
||||
|
||||
# Special target: https://github.com/docker/metadata-action#bake-definition
|
||||
target "meta-helper" {}
|
||||
|
||||
target "_common" {
|
||||
args = {
|
||||
GO_VERSION = GO_VERSION
|
||||
BUILD_TAGS = BUILD_TAGS
|
||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1
|
||||
}
|
||||
}
|
||||
|
||||
group "default" {
|
||||
targets = ["binary"]
|
||||
}
|
||||
|
||||
group "validate" {
|
||||
targets = ["lint", "vendor-validate", "license-validate"]
|
||||
}
|
||||
|
||||
target "lint" {
|
||||
inherits = ["_common"]
|
||||
target = "lint"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "license-validate" {
|
||||
target = "license-validate"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "license-update" {
|
||||
target = "license-update"
|
||||
output = ["."]
|
||||
}
|
||||
|
||||
target "vendor-validate" {
|
||||
inherits = ["_common"]
|
||||
target = "vendor-validate"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "vendor-update" {
|
||||
inherits = ["_common"]
|
||||
target = "vendor-update"
|
||||
output = ["."]
|
||||
}
|
||||
|
||||
target "test" {
|
||||
inherits = ["_common"]
|
||||
target = "test-coverage"
|
||||
output = [outdir("./bin/coverage/unit")]
|
||||
}
|
||||
|
||||
target "binary-with-coverage" {
|
||||
inherits = ["_common"]
|
||||
target = "binary"
|
||||
args = {
|
||||
BUILD_FLAGS = "-cover -covermode=atomic"
|
||||
}
|
||||
output = [outdir("./bin/build")]
|
||||
platforms = ["local"]
|
||||
}
|
||||
|
||||
target "binary" {
|
||||
inherits = ["_common"]
|
||||
target = "binary"
|
||||
output = [outdir("./bin/build")]
|
||||
platforms = ["local"]
|
||||
}
|
||||
|
||||
target "binary-cross" {
|
||||
inherits = ["binary"]
|
||||
platforms = [
|
||||
"darwin/amd64",
|
||||
"darwin/arm64",
|
||||
"linux/amd64",
|
||||
"linux/arm/v6",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64",
|
||||
"linux/ppc64le",
|
||||
"linux/riscv64",
|
||||
"linux/s390x",
|
||||
"windows/amd64",
|
||||
"windows/arm64"
|
||||
]
|
||||
}
|
||||
|
||||
target "release" {
|
||||
inherits = ["binary-cross"]
|
||||
target = "release"
|
||||
output = [outdir("./bin/release")]
|
||||
}
|
||||
|
||||
target "docs-validate" {
|
||||
inherits = ["_common"]
|
||||
target = "docs-validate"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "docs-update" {
|
||||
inherits = ["_common"]
|
||||
target = "docs-update"
|
||||
output = ["./docs"]
|
||||
}
|
||||
|
||||
target "image-cross" {
|
||||
inherits = ["meta-helper", "binary-cross"]
|
||||
output = ["type=image"]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
/_site
|
|
@ -1,10 +0,0 @@
|
|||
FROM ubuntu:13.10
|
||||
RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python
|
||||
RUN locale-gen en_US.UTF-8
|
||||
ADD Gemfile /code/
|
||||
ADD Gemfile.lock /code/
|
||||
WORKDIR /code
|
||||
RUN bundle install
|
||||
ADD . /code
|
||||
EXPOSE 4000
|
||||
CMD bundle exec jekyll build
|
|
@ -1,3 +0,0 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'github-pages'
|
|
@ -1,62 +0,0 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
RedCloth (4.2.9)
|
||||
blankslate (2.1.2.4)
|
||||
classifier (1.3.3)
|
||||
fast-stemmer (>= 1.0.0)
|
||||
colorator (0.1)
|
||||
commander (4.1.5)
|
||||
highline (~> 1.6.11)
|
||||
fast-stemmer (1.0.2)
|
||||
ffi (1.9.3)
|
||||
github-pages (12)
|
||||
RedCloth (= 4.2.9)
|
||||
jekyll (= 1.4.2)
|
||||
kramdown (= 1.2.0)
|
||||
liquid (= 2.5.4)
|
||||
maruku (= 0.7.0)
|
||||
rdiscount (= 2.1.7)
|
||||
redcarpet (= 2.3.0)
|
||||
highline (1.6.20)
|
||||
jekyll (1.4.2)
|
||||
classifier (~> 1.3)
|
||||
colorator (~> 0.1)
|
||||
commander (~> 4.1.3)
|
||||
liquid (~> 2.5.2)
|
||||
listen (~> 1.3)
|
||||
maruku (~> 0.7.0)
|
||||
pygments.rb (~> 0.5.0)
|
||||
redcarpet (~> 2.3.0)
|
||||
safe_yaml (~> 0.9.7)
|
||||
toml (~> 0.1.0)
|
||||
kramdown (1.2.0)
|
||||
liquid (2.5.4)
|
||||
listen (1.3.1)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9)
|
||||
rb-kqueue (>= 0.2)
|
||||
maruku (0.7.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
posix-spawn (0.3.8)
|
||||
pygments.rb (0.5.4)
|
||||
posix-spawn (~> 0.3.6)
|
||||
yajl-ruby (~> 1.1.0)
|
||||
rb-fsevent (0.9.4)
|
||||
rb-inotify (0.9.3)
|
||||
ffi (>= 0.5.0)
|
||||
rb-kqueue (0.2.0)
|
||||
ffi (>= 0.5.0)
|
||||
rdiscount (2.1.7)
|
||||
redcarpet (2.3.0)
|
||||
safe_yaml (0.9.7)
|
||||
toml (0.1.0)
|
||||
parslet (~> 1.5.0)
|
||||
yajl-ruby (1.1.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
github-pages
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue