mirror of
https://github.com/docker/compose.git
synced 2026-02-14 12:39:23 +08:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616777eb4a | ||
|
|
f44ca01fcf | ||
|
|
19a1454c2d | ||
|
|
aa297a9969 | ||
|
|
0d0a02cc6b | ||
|
|
3c641ed265 | ||
|
|
f41eec4e09 | ||
|
|
140dc519d3 | ||
|
|
279225896a | ||
|
|
a95cc4074a | ||
|
|
b4420c372b | ||
|
|
ce3700d334 | ||
|
|
e2a3fe9427 | ||
|
|
94465d57cc | ||
|
|
0dc64723c9 | ||
|
|
8891d9e2b5 | ||
|
|
6cd68a4bf2 | ||
|
|
a1984ca1de | ||
|
|
118b4f07e5 | ||
|
|
8714f983ac | ||
|
|
6bc50cb457 | ||
|
|
937fa2dc8f | ||
|
|
71ab6c9eef | ||
|
|
db88241698 | ||
|
|
723078c593 | ||
|
|
a1c50ef2c9 | ||
|
|
2977f4c897 | ||
|
|
cfdec21a7f | ||
|
|
b564cc5a17 | ||
|
|
43c444e890 | ||
|
|
b25a66bbd7 | ||
|
|
0e975262da | ||
|
|
c4d79e60b6 | ||
|
|
ddc4896b10 | ||
|
|
9b863549ee | ||
|
|
801678686c | ||
|
|
403d691abf | ||
|
|
b49b9ffe7e | ||
|
|
680763f8b7 | ||
|
|
1ed37ef7bd | ||
|
|
42169db166 | ||
|
|
d05f5f5fa7 | ||
|
|
5cc2c27abb | ||
|
|
7b7189fe00 | ||
|
|
de1d969c37 | ||
|
|
ab984d91af | ||
|
|
e413c2137a | ||
|
|
61845dd781 | ||
|
|
7a8d157871 | ||
|
|
88df5ede42 | ||
|
|
a7cc406187 | ||
|
|
126cb988c6 | ||
|
|
4c474fe029 | ||
|
|
209293e449 | ||
|
|
79af3cdd85 | ||
|
|
b80222fb07 | ||
|
|
ff53411d9d | ||
|
|
0ac0e29294 | ||
|
|
bc806da712 | ||
|
|
f72a604cbd | ||
|
|
e81168197a | ||
|
|
361194472e | ||
|
|
e7b488bb94 | ||
|
|
07eb8a598d | ||
|
|
8a9eae3190 | ||
|
|
48744dbe47 | ||
|
|
44c55e89c0 | ||
|
|
e016faac33 | ||
|
|
8ed2d8ad07 | ||
|
|
537f023a3b | ||
|
|
8b1b70833e | ||
|
|
06ae6d82cb | ||
|
|
84392d52c4 | ||
|
|
c87efed6a4 | ||
|
|
36926c41c6 | ||
|
|
24bf9789a6 | ||
|
|
cc4f194295 | ||
|
|
55cf579e02 | ||
|
|
1a7c1dfe7d | ||
|
|
8301dc8314 | ||
|
|
0d2beddf20 |
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -4,3 +4,15 @@ updates:
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
# docker/buildx + docker/cli + docker/docker require coordination to
|
||||
# ensure compatibility between them
|
||||
- dependency-name: "github.com/docker/buildx"
|
||||
# buildx is still 0.x
|
||||
update-types: ["version-update:semver-minor"]
|
||||
- dependency-name: "github.com/docker/cli"
|
||||
# docker/cli uses CalVer rather than SemVer
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "github.com/docker/docker"
|
||||
# docker/docker uses CalVer rather than SemVer
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -19,10 +19,12 @@ on:
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18.5" # for non sandboxed e2e tests
|
||||
DESTDIR: "./bin"
|
||||
DOCKER_CLI_VERSION: "20.10.17"
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -143,7 +145,8 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
cache: true
|
||||
-
|
||||
name: Setup docker CLI
|
||||
@@ -182,6 +185,9 @@ jobs:
|
||||
make e2e-compose-standalone
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: write # to create a release (ncipollo/release-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- binary
|
||||
|
||||
5
.github/workflows/docs.yml
vendored
5
.github/workflows/docs.yml
vendored
@@ -4,8 +4,13 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
open-pr:
|
||||
permissions:
|
||||
contents: write # to create branch (peter-evans/create-pull-request)
|
||||
pull-requests: write # to create a PR (peter-evans/create-pull-request)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
|
||||
74
.github/workflows/merge.yml
vendored
Normal file
74
.github/workflows/merge.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: merge
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'v2'
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
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@v3
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
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
|
||||
|
||||
19
.github/workflows/rebase.yml
vendored
19
.github/workflows/rebase.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Automatic Rebase
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -5,7 +5,6 @@ linters:
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
enable:
|
||||
- deadcode
|
||||
- depguard
|
||||
- errcheck
|
||||
- gocritic
|
||||
@@ -21,13 +20,15 @@ linters:
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
linters-settings:
|
||||
revive:
|
||||
rules:
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
depguard:
|
||||
list-type: denylist
|
||||
include-go-root: true
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.18.5
|
||||
ARG GO_VERSION=1.19.1
|
||||
ARG XX_VERSION=1.1.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.47.3
|
||||
ARG GOLANGCI_LINT_VERSION=v1.49.0
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e,kube"
|
||||
|
||||
15
Makefile
15
Makefile
@@ -18,13 +18,20 @@ VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
||||
GO_LDFLAGS ?= -s -w -X ${PKG}/internal.Version=${VERSION}
|
||||
GO_BUILDTAGS ?= e2e,kube
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DETECTED_OS = Windows
|
||||
else
|
||||
DETECTED_OS = $(shell uname -s)
|
||||
endif
|
||||
ifeq ($(DETECTED_OS),Linux)
|
||||
MOBY_DOCKER=/usr/bin/docker
|
||||
endif
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
ifeq ($(DETECTED_OS),Darwin)
|
||||
MOBY_DOCKER=/Applications/Docker.app/Contents/Resources/bin/docker
|
||||
endif
|
||||
ifeq ($(DETECTED_OS),Windows)
|
||||
BINARY_EXT=.exe
|
||||
endif
|
||||
|
||||
TEST_FLAGS?=
|
||||
E2E_TEST?=
|
||||
@@ -40,7 +47,7 @@ all: build
|
||||
|
||||
.PHONY: build ## Build the compose cli-plugin
|
||||
build:
|
||||
CGO_ENABLED=0 GO111MODULE=on go build -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(DESTDIR)/docker-compose" ./cmd
|
||||
CGO_ENABLED=0 GO111MODULE=on go build -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(DESTDIR)/docker-compose$(BINARY_EXT)" ./cmd
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
|
||||
@@ -35,12 +35,12 @@ You can download Docker Compose binaries from the
|
||||
|
||||
Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins`
|
||||
|
||||
Or copy it into one of these folders for installing it system-wide:
|
||||
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 to make the downloaded file executable with `chmod +x`)
|
||||
(might require making the downloaded file executable with `chmod +x`)
|
||||
|
||||
|
||||
Quick Start
|
||||
|
||||
@@ -23,6 +23,13 @@ import (
|
||||
"github.com/docker/compose/v2/cmd/compose"
|
||||
)
|
||||
|
||||
func getCompletionCommands() []string {
|
||||
return []string{
|
||||
"__complete",
|
||||
"__completeNoDesc",
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolFlags() []string {
|
||||
return []string{
|
||||
"--debug", "-D",
|
||||
@@ -50,6 +57,10 @@ func Convert(args []string) []string {
|
||||
l := len(args)
|
||||
for i := 0; i < l; i++ {
|
||||
arg := args[i]
|
||||
if contains(getCompletionCommands(), arg) {
|
||||
command = append([]string{arg}, command...)
|
||||
continue
|
||||
}
|
||||
if len(arg) > 0 && arg[0] != '-' {
|
||||
// not a top-level flag anymore, keep the rest of the command unmodified
|
||||
if arg == compose.PluginName {
|
||||
|
||||
@@ -102,7 +102,7 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
return runBuild(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
|
||||
|
||||
@@ -19,6 +19,7 @@ package compose
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -27,11 +28,11 @@ type validArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]s
|
||||
|
||||
func noCompletion() validArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{}, cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
}
|
||||
|
||||
func serviceCompletion(p *projectOptions) validArgsFn {
|
||||
func completeServiceNames(p *projectOptions) validArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
project, err := p.toProject(nil)
|
||||
if err != nil {
|
||||
@@ -46,3 +47,21 @@ func serviceCompletion(p *projectOptions) validArgsFn {
|
||||
return serviceNames, 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,17 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
)
|
||||
c.Flags().SetInterspersed(false)
|
||||
opts.addProjectFlags(c.Flags())
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"project-name",
|
||||
completeProjectNames(backend),
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"file",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
|
||||
},
|
||||
)
|
||||
|
||||
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
||||
c.Flags().MarkHidden("version") //nolint:errcheck
|
||||
|
||||
@@ -18,6 +18,7 @@ package compose
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -92,7 +93,7 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
|
||||
return runConvert(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
|
||||
@@ -112,7 +113,7 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runConvert(ctx context.Context, backend api.Service, opts convertOptions, services []string) error {
|
||||
var json []byte
|
||||
var content []byte
|
||||
project, err := opts.toProject(services,
|
||||
cli.WithInterpolation(!opts.noInterpolate),
|
||||
cli.WithResolvedPaths(true),
|
||||
@@ -136,7 +137,7 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s
|
||||
}
|
||||
}
|
||||
|
||||
json, err = backend.Convert(ctx, project, api.ConvertOptions{
|
||||
content, err = backend.Convert(ctx, project, api.ConvertOptions{
|
||||
Format: opts.Format,
|
||||
Output: opts.Output,
|
||||
})
|
||||
@@ -144,19 +145,23 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.noInterpolate {
|
||||
content = escapeDollarSign(content)
|
||||
}
|
||||
|
||||
if opts.quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out io.Writer = os.Stdout
|
||||
if opts.Output != "" && len(json) > 0 {
|
||||
if opts.Output != "" && len(content) > 0 {
|
||||
file, err := os.Create(opts.Output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = bufio.NewWriter(file)
|
||||
}
|
||||
_, err = fmt.Fprint(out, string(json))
|
||||
_, err = fmt.Fprint(out, string(content))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -237,3 +242,9 @@ func runConfigImages(opts convertOptions, services []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func escapeDollarSign(marshal []byte) []byte {
|
||||
dollar := []byte{'$'}
|
||||
escDollar := []byte{'$', '$'}
|
||||
return bytes.ReplaceAll(marshal, dollar, escDollar)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func copyCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
opts.destination = args[1]
|
||||
return runCopy(ctx, backend, opts)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
||||
flags := copyCmd.Flags()
|
||||
|
||||
@@ -70,7 +70,7 @@ func createCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
QuietPull: false,
|
||||
})
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
|
||||
|
||||
@@ -43,7 +43,7 @@ func eventsCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runEvents(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
|
||||
|
||||
@@ -61,7 +61,7 @@ func execCommand(p *projectOptions, dockerCli command.Cli, backend api.Service)
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runExec(ctx, backend, opts)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
||||
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.")
|
||||
|
||||
@@ -48,7 +48,7 @@ func imagesCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runImages(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
return imgCmd
|
||||
|
||||
@@ -42,7 +42,7 @@ func killCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runKill(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@@ -49,7 +49,7 @@ func logsCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runLogs(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := logsCmd.Flags()
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output.")
|
||||
@@ -63,12 +63,13 @@ func logsCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
|
||||
func runLogs(ctx context.Context, backend api.Service, opts logsOptions, services []string) error {
|
||||
projectName, err := opts.toProjectName()
|
||||
project, name, err := opts.projectOrName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
|
||||
return backend.Logs(ctx, projectName, consumer, api.LogOptions{
|
||||
return backend.Logs(ctx, name, consumer, api.LogOptions{
|
||||
Project: project,
|
||||
Services: services,
|
||||
Follow: opts.follow,
|
||||
Tail: opts.tail,
|
||||
|
||||
@@ -38,7 +38,7 @@ func pauseCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPause(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func unpauseCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runUnPause(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func portCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPort(ctx, backend, opts, args[0])
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
|
||||
cmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if service has multiple replicas")
|
||||
|
||||
@@ -78,7 +78,7 @@ func psCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPs(ctx, backend, args, opts)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := psCmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json]")
|
||||
|
||||
@@ -54,7 +54,7 @@ func pullCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPull(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information")
|
||||
|
||||
@@ -41,7 +41,7 @@ func pushCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPush(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Any data which is not in a volume will be lost.`,
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRemove(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
f := cmd.Flags()
|
||||
f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal")
|
||||
|
||||
@@ -40,7 +40,7 @@ func restartCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRestart(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := restartCmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
|
||||
|
||||
@@ -143,7 +143,7 @@ func runCommand(p *projectOptions, dockerCli command.Cli, backend api.Service) *
|
||||
opts.ignoreOrphans = strings.ToLower(ignore) == "true"
|
||||
return runRun(ctx, backend, project, opts)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
|
||||
|
||||
@@ -37,7 +37,7 @@ func startCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStart(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
return startCmd
|
||||
}
|
||||
@@ -51,5 +51,6 @@ func runStart(ctx context.Context, backend api.Service, opts startOptions, servi
|
||||
return backend.Start(ctx, name, api.StartOptions{
|
||||
AttachTo: services,
|
||||
Project: project,
|
||||
Services: services,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func stopCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runStop(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
|
||||
|
||||
@@ -44,7 +44,7 @@ func topCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runTop(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
return topCmd
|
||||
}
|
||||
|
||||
35
cmd/compose/tracing.go
Normal file
35
cmd/compose/tracing.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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/moby/buildkit/util/tracing/detect"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
||||
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
|
||||
)
|
||||
|
||||
func init() {
|
||||
detect.ServiceName = "compose"
|
||||
// do not log tracing errors to stdio
|
||||
otel.SetErrorHandler(skipErrors{})
|
||||
}
|
||||
|
||||
type skipErrors struct{}
|
||||
|
||||
func (skipErrors) Handle(err error) {}
|
||||
@@ -109,7 +109,7 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
}
|
||||
return runUp(ctx, backend, create, up, project, services)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := upCmd.Flags()
|
||||
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
|
||||
|
||||
@@ -34,14 +34,13 @@ import (
|
||||
|
||||
func pluginMain() {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
lazyInit := api.NewServiceProxy()
|
||||
cmd := commands.RootCommand(dockerCli, lazyInit)
|
||||
serviceProxy := api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli))
|
||||
cmd := commands.RootCommand(dockerCli, serviceProxy)
|
||||
originalPreRun := cmd.PersistentPreRunE
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
lazyInit.WithService(compose.NewComposeService(dockerCli))
|
||||
if originalPreRun != nil {
|
||||
return originalPreRun(cmd, args)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
variable "GO_VERSION" {
|
||||
default = "1.18.5"
|
||||
default = "1.19.1"
|
||||
}
|
||||
|
||||
variable "BUILD_TAGS" {
|
||||
|
||||
34
go.mod
34
go.mod
@@ -1,28 +1,28 @@
|
||||
module github.com/docker/compose/v2
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.5
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/cnabio/cnab-to-oci v0.3.6
|
||||
github.com/compose-spec/compose-go v1.5.0
|
||||
github.com/cnabio/cnab-to-oci v0.3.7
|
||||
github.com/compose-spec/compose-go v1.6.0
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.6.8
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0
|
||||
github.com/docker/buildx v0.8.2 // when updating, also update the replace rules accordingly
|
||||
github.com/docker/cli v20.10.17+incompatible
|
||||
github.com/docker/cli-docs-tool v0.5.0
|
||||
github.com/docker/docker v20.10.17+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/mattn/go-isatty v0.0.16
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/moby/buildkit v0.10.4
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
|
||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
|
||||
@@ -32,7 +32,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/theupdateframework/notary v0.7.0
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
gotest.tools/v3 v3.3.0
|
||||
@@ -55,7 +55,7 @@ require (
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
@@ -101,15 +101,15 @@ require (
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
|
||||
go.opentelemetry.io/otel v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel v1.10.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.10.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
@@ -122,7 +122,7 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apimachinery v0.24.1 // indirect; see replace for the actual version used
|
||||
k8s.io/client-go v0.24.1 // indirect; see replace for the actual version used
|
||||
k8s.io/client-go v0.24.1 // see replace for the actual version used
|
||||
k8s.io/klog/v2 v2.60.1 // indirect
|
||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
@@ -130,9 +130,17 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c // indirect
|
||||
github.com/zmap/zlint v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 // indirect
|
||||
k8s.io/api v0.24.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
48
go.sum
48
go.sum
@@ -56,8 +56,8 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
|
||||
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
|
||||
github.com/AkihiroSuda/containerd-fuse-overlayfs v1.0.0/go.mod h1:0mMDvQFeLbbn1Wy8P2j3hwFhqBq+FKn8OZPno8WLmp8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU=
|
||||
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
|
||||
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
|
||||
@@ -246,6 +246,7 @@ github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMS
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -270,8 +271,8 @@ github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e/go.mod h1:yMWuSON
|
||||
github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw=
|
||||
github.com/cnabio/cnab-go v0.23.4 h1:jplQcSnvFyQlD6swiqL3BmqRnhbnS+lc/EKdBLH9E80=
|
||||
github.com/cnabio/cnab-go v0.23.4/go.mod h1:9EmgHR51LFqQStzaC+xHPJlkD4OPsF6Ev5Y8e/YHEns=
|
||||
github.com/cnabio/cnab-to-oci v0.3.6 h1:QVvy4WjQpGyf20xbbeYtRObX+pB8cWNuvvT/e4w1DoQ=
|
||||
github.com/cnabio/cnab-to-oci v0.3.6/go.mod h1:AvVNl0Hh3VBk1zqeLdyE5S3bTQ5EsZPPF4mUUJYyy1Y=
|
||||
github.com/cnabio/cnab-to-oci v0.3.7 h1:wA2AG3HQMaJZhWlr3zsfVoa2m5B1R/SP+YcoFuNfP9o=
|
||||
github.com/cnabio/cnab-to-oci v0.3.7/go.mod h1:AvVNl0Hh3VBk1zqeLdyE5S3bTQ5EsZPPF4mUUJYyy1Y=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -286,8 +287,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
|
||||
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/compose-spec/compose-go v1.2.1/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8=
|
||||
github.com/compose-spec/compose-go v1.5.0 h1:yOmYpIm13pYt2o+oKVe/JAD6o2Tv+eUyOcRhf0qF4fA=
|
||||
github.com/compose-spec/compose-go v1.5.0/go.mod h1:l7RUULbFFLzlQHuxtJr7SVLyWdqEpbJEGTWCgcu6Eqw=
|
||||
github.com/compose-spec/compose-go v1.6.0 h1:7Ol/UULMUtbPmB0EYrETASRoum821JpOh/XaEf+hN+Q=
|
||||
github.com/compose-spec/compose-go v1.6.0/go.mod h1:os+Ulh2jlZxY1XT1hbciERadjSUU/BtZ6+gcN7vD7J0=
|
||||
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
|
||||
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
|
||||
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
|
||||
@@ -449,8 +450,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e/go.mod h1:xpWTC2KnJMiDLkoawhsPQcXjvwATEBcbq0xevG2YR9M=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f h1:3NCYdjXycNd/Xn/iICZzmxkiDX1e1cjTHjbMAz+wRVk=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0 h1:0UuPq7m6stSY6at1v5PLo0zzYTpailcwjhmkJpgnGBY=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/buildx v0.8.2 h1:dsd3F0hhmUydFX/KFrvbK81JvlTA4T3Iy0lwDJt4PsU=
|
||||
github.com/docker/buildx v0.8.2/go.mod h1:5sMOfNwOmO2jy/MxBL4ySk2LoLIG1tQFu2EU8wbKa34=
|
||||
@@ -482,8 +483,9 @@ github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw
|
||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libnetwork v0.8.0-dev.2.0.20200917202933-d0951081b35f/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8=
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/libtrust v0.0.0-20150526203908-9cbd2a1374f4/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
@@ -497,6 +499,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8=
|
||||
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
@@ -555,8 +558,9 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg
|
||||
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
@@ -760,6 +764,7 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
|
||||
github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
|
||||
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
|
||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||
github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
@@ -1021,6 +1026,7 @@ github.com/moby/buildkit v0.10.4 h1:FvC+buO8isGpUFZ1abdSLdGHZVqg9sqI4BbFL8tlzP4=
|
||||
github.com/moby/buildkit v0.10.4/go.mod h1:Yajz9vt1Zw5q9Pp4pdb3TCSUXJBIroIQGQ3TTs/sLug=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/sys/mount v0.1.0/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74=
|
||||
github.com/moby/sys/mount v0.1.1/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74=
|
||||
@@ -1040,8 +1046,9 @@ github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
|
||||
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
|
||||
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
|
||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -1234,6 +1241,7 @@ github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+y
|
||||
github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 h1:ka9QPuQg2u4LGipiZGsgkg3rJCo4iIUCy75FddM0GRQ=
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
@@ -1459,18 +1467,22 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0/go.mod h1:
|
||||
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
|
||||
go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
|
||||
go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk=
|
||||
go.opentelemetry.io/otel v1.4.1 h1:QbINgGDDcoQUoMJa2mMaWno49lja9sHwp6aoa2n3a4g=
|
||||
go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4=
|
||||
go.opentelemetry.io/otel v1.10.0 h1:Y7DTJMR6zs1xkS/upamJYk0SxxN4C9AqRd77jmZnyY4=
|
||||
go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.4.1/go.mod h1:ZW7vkOu9nC1CxsD8bHNHCia5JUbwP39vxgd1q4Z5rCI=
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 h1:imIM3vRDMyZK1ypQlQlO+brE22I9lRhJsBDXpDWjlz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 h1:WPpPsAAs8I2rA47v5u0558meKmmwm1Dj99ZbqCV8sZ8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1/go.mod h1:o5RW5o2pKpJLD5dNTCmjF1DorYwMeFJmb/rKr5sLaa8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 h1:AxqDiGk8CorEXStMDZF5Hz9vo9Z7ZZ+I5m8JRl/ko40=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1/go.mod h1:c6E4V3/U+miqjs/8l950wggHGL1qzlp0Ypj9xoGrPqo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 h1:8qOago/OqoFclMUUj/184tZyRdDZFpcejSjbk5Jrl6Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1/go.mod h1:VwYo0Hak6Efuy0TXsZs8o1hnV3dHDPNtDbycG0hI8+M=
|
||||
go.opentelemetry.io/otel/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk=
|
||||
go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw=
|
||||
@@ -1487,8 +1499,9 @@ go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4
|
||||
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
|
||||
go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
|
||||
go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE=
|
||||
go.opentelemetry.io/otel/trace v1.4.1 h1:O+16qcdTrT7zxv2J6GejTPFinSwA++cYerC5iSiF8EQ=
|
||||
go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc=
|
||||
go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E=
|
||||
go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c=
|
||||
@@ -1497,6 +1510,7 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
@@ -1643,8 +1657,8 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1678,8 +1692,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
type Service interface {
|
||||
// Build executes the equivalent to a `compose build`
|
||||
Build(ctx context.Context, project *types.Project, options BuildOptions) error
|
||||
// Push executes the equivalent ot a `compose push`
|
||||
// Push executes the equivalent to a `compose push`
|
||||
Push(ctx context.Context, project *types.Project, options PushOptions) error
|
||||
// Pull executes the equivalent of a `compose pull`
|
||||
Pull(ctx context.Context, project *types.Project, options PullOptions) error
|
||||
@@ -129,6 +129,8 @@ type StartOptions struct {
|
||||
ExitCodeFrom string
|
||||
// Wait won't return until containers reached the running|healthy state
|
||||
Wait bool
|
||||
// Services passed in the command line to be started
|
||||
Services []string
|
||||
}
|
||||
|
||||
// RestartOptions group options of the Restart API
|
||||
@@ -378,6 +380,7 @@ type ServiceStatus struct {
|
||||
|
||||
// LogOptions defines optional parameters for the `Log` API
|
||||
type LogOptions struct {
|
||||
Project *types.Project
|
||||
Services []string
|
||||
Tail string
|
||||
Since string
|
||||
@@ -429,7 +432,7 @@ type Stack struct {
|
||||
|
||||
// LogConsumer is a callback to process log messages from services
|
||||
type LogConsumer interface {
|
||||
Log(service, container, message string)
|
||||
Log(containerName, service, message string)
|
||||
Status(container, msg string)
|
||||
Register(container string)
|
||||
}
|
||||
@@ -439,7 +442,11 @@ type ContainerEventListener func(event ContainerEvent)
|
||||
|
||||
// ContainerEvent notify an event has been collected on source container implementing Service
|
||||
type ContainerEvent struct {
|
||||
Type int
|
||||
Type int
|
||||
// Container is the name of the container _without the project prefix_.
|
||||
//
|
||||
// This is only suitable for display purposes within Compose, as it's
|
||||
// not guaranteed to be unique across services.
|
||||
Container string
|
||||
Service string
|
||||
Line string
|
||||
|
||||
@@ -51,8 +51,10 @@ const (
|
||||
ImageDigestLabel = "com.docker.compose.image"
|
||||
// DependenciesLabel stores service dependencies
|
||||
DependenciesLabel = "com.docker.compose.depends_on"
|
||||
// VersionLabel stores the compose tool version used to run application
|
||||
// VersionLabel stores the compose tool version used to build/run application
|
||||
VersionLabel = "com.docker.compose.version"
|
||||
// ImageBuilderLabel stores the builder (classic or BuildKit) used to produce the image.
|
||||
ImageBuilderLabel = "com.docker.compose.image.builder"
|
||||
)
|
||||
|
||||
// ComposeVersion is the compose tool version as declared by label VersionLabel
|
||||
|
||||
@@ -81,6 +81,18 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
Attrs: map[string]string{"ref": image},
|
||||
})
|
||||
}
|
||||
buildOptions.Exports = []bclient.ExportEntry{{
|
||||
Type: "docker",
|
||||
Attrs: map[string]string{
|
||||
"load": "true",
|
||||
},
|
||||
}}
|
||||
if len(buildOptions.Platforms) > 1 {
|
||||
buildOptions.Exports = []bclient.ExportEntry{{
|
||||
Type: "image",
|
||||
Attrs: map[string]string{},
|
||||
}}
|
||||
}
|
||||
opts[imageName] = buildOptions
|
||||
}
|
||||
|
||||
@@ -138,7 +150,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
if project.Services[i].Labels == nil {
|
||||
project.Services[i].Labels = types.Labels{}
|
||||
}
|
||||
project.Services[i].CustomLabels[api.ImageDigestLabel] = digest
|
||||
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -161,6 +173,15 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt.Exports = []bclient.ExportEntry{{
|
||||
Type: "docker",
|
||||
Attrs: map[string]string{
|
||||
"load": "true",
|
||||
},
|
||||
}}
|
||||
if opt.Platforms, err = useDockerDefaultOrServicePlatform(project, service, true); err != nil {
|
||||
opt.Platforms = []specs.Platform{}
|
||||
}
|
||||
opts[imageName] = opt
|
||||
continue
|
||||
}
|
||||
@@ -204,7 +225,7 @@ func (s *composeService) doBuild(ctx context.Context, project *types.Project, op
|
||||
if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
|
||||
return s.doBuildClassic(ctx, project, opts)
|
||||
}
|
||||
return s.doBuildBuildkit(ctx, project, opts, mode)
|
||||
return s.doBuildBuildkit(ctx, opts, mode)
|
||||
}
|
||||
|
||||
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, sshKeys []types.SSHKey) (build.Options, error) {
|
||||
@@ -213,20 +234,9 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
|
||||
buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
|
||||
|
||||
var plats []specs.Platform
|
||||
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
|
||||
p, err := platforms.Parse(platform)
|
||||
if err != nil {
|
||||
return build.Options{}, err
|
||||
}
|
||||
plats = append(plats, p)
|
||||
}
|
||||
if service.Platform != "" {
|
||||
p, err := platforms.Parse(service.Platform)
|
||||
if err != nil {
|
||||
return build.Options{}, err
|
||||
}
|
||||
plats = append(plats, p)
|
||||
plats, err := addPlatforms(project, service)
|
||||
if err != nil {
|
||||
return build.Options{}, err
|
||||
}
|
||||
|
||||
cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
|
||||
@@ -261,6 +271,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
tags = append(tags, service.Build.Tags...)
|
||||
}
|
||||
|
||||
imageLabels := getImageBuildLabels(project, service)
|
||||
|
||||
return build.Options{
|
||||
Inputs: build.Inputs{
|
||||
ContextPath: service.Build.Context,
|
||||
@@ -275,7 +287,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
Target: service.Build.Target,
|
||||
Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
|
||||
Platforms: plats,
|
||||
Labels: service.Build.Labels,
|
||||
Labels: imageLabels,
|
||||
NetworkMode: service.Build.Network,
|
||||
ExtraHosts: service.Build.ExtraHosts.AsList(),
|
||||
Session: sessionConfig,
|
||||
@@ -325,7 +337,6 @@ func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
|
||||
}
|
||||
|
||||
func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
|
||||
|
||||
var sources []secretsprovider.Source
|
||||
for _, secret := range service.Build.Secrets {
|
||||
config := project.Secrets[secret.Source]
|
||||
@@ -350,3 +361,70 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
|
||||
}
|
||||
return secretsprovider.NewSecretProvider(store), nil
|
||||
}
|
||||
|
||||
func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
|
||||
plats, err := useDockerDefaultOrServicePlatform(project, service, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, buildPlatform := range service.Build.Platforms {
|
||||
p, err := platforms.Parse(buildPlatform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !utils.Contains(plats, p) {
|
||||
plats = append(plats, p)
|
||||
}
|
||||
}
|
||||
return plats, nil
|
||||
}
|
||||
|
||||
func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
|
||||
ret := make(types.Labels)
|
||||
if service.Build != nil {
|
||||
for k, v := range service.Build.Labels {
|
||||
ret.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
ret.Add(api.VersionLabel, api.ComposeVersion)
|
||||
ret.Add(api.ProjectLabel, project.Name)
|
||||
ret.Add(api.ServiceLabel, service.Name)
|
||||
return ret
|
||||
}
|
||||
|
||||
func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
|
||||
var plats []specs.Platform
|
||||
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
|
||||
if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
|
||||
return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
|
||||
}
|
||||
p, err := platforms.Parse(platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plats = append(plats, p)
|
||||
}
|
||||
return plats, nil
|
||||
}
|
||||
|
||||
func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
|
||||
plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
|
||||
if (len(plats) > 0 && useOnePlatform) || err != nil {
|
||||
return plats, err
|
||||
}
|
||||
|
||||
if service.Platform != "" && !utils.StringContains(service.Build.Platforms, service.Platform) {
|
||||
if len(service.Build.Platforms) > 0 {
|
||||
return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
|
||||
}
|
||||
// User defined a service platform and no build platforms, so we should keep the one define on the service level
|
||||
p, err := platforms.Parse(service.Platform)
|
||||
if !utils.Contains(plats, p) {
|
||||
plats = append(plats, p)
|
||||
}
|
||||
return plats, err
|
||||
}
|
||||
return plats, nil
|
||||
}
|
||||
|
||||
@@ -18,27 +18,36 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
ctxkube "github.com/docker/buildx/driver/kubernetes/context"
|
||||
"github.com/docker/buildx/store"
|
||||
"github.com/docker/buildx/store/storeutil"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
ctxstore "github.com/docker/cli/cli/context/store"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/driver"
|
||||
_ "github.com/docker/buildx/driver/docker" //nolint:blank-imports
|
||||
_ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports
|
||||
_ "github.com/docker/buildx/driver/kubernetes" //nolint:blank-imports
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
|
||||
const drivername = "default"
|
||||
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient(), s.configFile(), nil, nil, nil, nil, nil, project.WorkingDir)
|
||||
func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]build.Options, mode string) (map[string]string, error) {
|
||||
dis, err := s.getDrivers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driverInfo := []build.DriverInfo{
|
||||
{
|
||||
Name: drivername,
|
||||
Driver: d,
|
||||
},
|
||||
}
|
||||
|
||||
// Progress needs its own context that lives longer than the
|
||||
// build one otherwise it won't read all the messages from
|
||||
@@ -47,8 +56,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
|
||||
defer cancel()
|
||||
w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode)
|
||||
|
||||
// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
|
||||
response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile().Filename), w)
|
||||
response, err := build.Build(ctx, dis, opts, &internalAPI{dockerCli: s.dockerCli}, filepath.Dir(s.configFile().Filename), w)
|
||||
errW := w.Wait()
|
||||
if err == nil {
|
||||
err = errW
|
||||
@@ -71,3 +79,187 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
|
||||
|
||||
return imagesBuilt, err
|
||||
}
|
||||
|
||||
func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, error) { //nolint:gocyclo
|
||||
txn, release, err := storeutil.GetStore(s.dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer release()
|
||||
|
||||
ng, err := storeutil.GetCurrentInstance(txn, s.dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dis := make([]build.DriverInfo, len(ng.Nodes))
|
||||
var f driver.Factory
|
||||
if ng.Driver != "" {
|
||||
factories := driver.GetFactories()
|
||||
for _, fac := range factories {
|
||||
if fac.Name() == ng.Driver {
|
||||
f = fac
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f == nil {
|
||||
if f = driver.GetFactory(ng.Driver, true); f == nil {
|
||||
return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ep := ng.Nodes[0].Endpoint
|
||||
dockerapi, err := clientForEndpoint(s.dockerCli, ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err = driver.GetDefaultFactory(ctx, dockerapi, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ng.Driver = f.Name()
|
||||
}
|
||||
|
||||
imageopt, err := storeutil.GetImageConfig(s.dockerCli, ng)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
for i, n := range ng.Nodes {
|
||||
func(i int, n store.Node) {
|
||||
eg.Go(func() error {
|
||||
di := build.DriverInfo{
|
||||
Name: n.Name,
|
||||
Platform: n.Platforms,
|
||||
ProxyConfig: storeutil.GetProxyConfig(s.dockerCli),
|
||||
}
|
||||
defer func() {
|
||||
dis[i] = di
|
||||
}()
|
||||
|
||||
dockerapi, err := clientForEndpoint(s.dockerCli, n.Endpoint)
|
||||
if err != nil {
|
||||
di.Err = err
|
||||
return nil
|
||||
}
|
||||
// TODO: replace the following line with dockerclient.WithAPIVersionNegotiation option in clientForEndpoint
|
||||
dockerapi.NegotiateAPIVersion(ctx)
|
||||
|
||||
contextStore := s.dockerCli.ContextStore()
|
||||
|
||||
var kcc driver.KubeClientConfig
|
||||
kcc, err = configFromContext(n.Endpoint, contextStore)
|
||||
if err != nil {
|
||||
// err is returned if n.Endpoint is non-context name like "unix:///var/run/docker.sock".
|
||||
// try again with name="default".
|
||||
// FIXME: n should retain real context name.
|
||||
kcc, err = configFromContext("default", contextStore)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
tryToUseKubeConfigInCluster := false
|
||||
if kcc == nil {
|
||||
tryToUseKubeConfigInCluster = true
|
||||
} else {
|
||||
if _, err := kcc.ClientConfig(); err != nil {
|
||||
tryToUseKubeConfigInCluster = true
|
||||
}
|
||||
}
|
||||
if tryToUseKubeConfigInCluster {
|
||||
kccInCluster := driver.KubeClientConfigInCluster{}
|
||||
if _, err := kccInCluster.ClientConfig(); err == nil {
|
||||
logrus.Debug("using kube config in cluster")
|
||||
kcc = kccInCluster
|
||||
}
|
||||
}
|
||||
|
||||
d, err := driver.GetDriver(ctx, "buildx_buildkit_"+n.Name, f, dockerapi, imageopt.Auth, kcc, n.Flags, n.Files, n.DriverOpts, n.Platforms, "")
|
||||
if err != nil {
|
||||
di.Err = err
|
||||
return nil
|
||||
}
|
||||
di.Driver = d
|
||||
di.ImageOpt = imageopt
|
||||
return nil
|
||||
})
|
||||
}(i, n)
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dis, nil
|
||||
}
|
||||
|
||||
func clientForEndpoint(dockerCli command.Cli, name string) (dockerclient.APIClient, error) {
|
||||
list, err := dockerCli.ContextStore().List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, l := range list {
|
||||
if l.Name != name {
|
||||
continue
|
||||
}
|
||||
dep, ok := l.Endpoints["docker"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("context %q does not have a Docker endpoint", name)
|
||||
}
|
||||
epm, ok := dep.(docker.EndpointMeta)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("endpoint %q is not of type EndpointMeta, %T", dep, dep)
|
||||
}
|
||||
ep, err := docker.WithTLSData(dockerCli.ContextStore(), name, epm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientOpts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dockerclient.NewClientWithOpts(clientOpts...)
|
||||
}
|
||||
|
||||
ep := docker.Endpoint{
|
||||
EndpointMeta: docker.EndpointMeta{
|
||||
Host: name,
|
||||
},
|
||||
}
|
||||
|
||||
clientOpts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dockerclient.NewClientWithOpts(clientOpts...)
|
||||
}
|
||||
|
||||
func configFromContext(endpointName string, s ctxstore.Reader) (clientcmd.ClientConfig, error) {
|
||||
if strings.HasPrefix(endpointName, "kubernetes://") {
|
||||
u, _ := url.Parse(endpointName)
|
||||
if kubeconfig := u.Query().Get("kubeconfig"); kubeconfig != "" {
|
||||
_ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfig)
|
||||
}
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
apiConfig, err := rules.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}), nil
|
||||
}
|
||||
return ctxkube.ConfigFromContext(endpointName, s)
|
||||
}
|
||||
|
||||
type internalAPI struct {
|
||||
dockerCli command.Cli
|
||||
}
|
||||
|
||||
func (a *internalAPI) DockerAPI(name string) (dockerclient.APIClient, error) {
|
||||
if name == "" {
|
||||
name = a.dockerCli.CurrentContext()
|
||||
}
|
||||
return clientForEndpoint(a.dockerCli, name)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,15 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.Platforms) > 1 {
|
||||
return "", errors.Errorf("this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder")
|
||||
}
|
||||
|
||||
if options.Labels == nil {
|
||||
options.Labels = make(map[string]string)
|
||||
}
|
||||
options.Labels[api.ImageBuilderLabel] = "classic"
|
||||
|
||||
switch {
|
||||
case isLocalDir(specifiedContext):
|
||||
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -30,11 +29,12 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
// NewComposeService create a local implementation of the compose.Service API
|
||||
@@ -95,28 +95,14 @@ func getContainerNameWithoutProject(c moby.Container) string {
|
||||
func (s *composeService) Convert(ctx context.Context, project *types.Project, options api.ConvertOptions) ([]byte, error) {
|
||||
switch options.Format {
|
||||
case "json":
|
||||
marshal, err := json.MarshalIndent(project, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return escapeDollarSign(marshal), nil
|
||||
return json.MarshalIndent(project, "", " ")
|
||||
case "yaml":
|
||||
marshal, err := yaml.Marshal(project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return escapeDollarSign(marshal), nil
|
||||
return yaml.Marshal(project)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format %q", options)
|
||||
}
|
||||
}
|
||||
|
||||
func escapeDollarSign(marshal []byte) []byte {
|
||||
dollar := []byte{'$'}
|
||||
escDollar := []byte{'$', '$'}
|
||||
return bytes.ReplaceAll(marshal, dollar, escDollar)
|
||||
}
|
||||
|
||||
// projectFromName builds a types.Project based on actual resources with compose labels set
|
||||
func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
|
||||
project := &types.Project{
|
||||
|
||||
@@ -23,12 +23,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/golang/mock/gomock"
|
||||
"gotest.tools/assert"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestContainerName(t *testing.T) {
|
||||
@@ -77,7 +78,9 @@ func TestServiceLinks(t *testing.T) {
|
||||
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
s.Links = []string{"db"}
|
||||
@@ -99,7 +102,9 @@ func TestServiceLinks(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
s.Links = []string{"db:db"}
|
||||
@@ -121,7 +126,9 @@ func TestServiceLinks(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
s.Links = []string{"db:dbname"}
|
||||
@@ -143,7 +150,9 @@ func TestServiceLinks(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
s.Links = []string{"db:dbname"}
|
||||
@@ -169,7 +178,9 @@ func TestServiceLinks(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
s.Links = []string{}
|
||||
@@ -203,7 +214,9 @@ func TestWaitDependencies(t *testing.T) {
|
||||
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
t.Run("should skip dependencies with scale 0", func(t *testing.T) {
|
||||
|
||||
@@ -125,7 +125,8 @@ func prepareVolumes(p *types.Project) error {
|
||||
p.Services[i].DependsOn = make(types.DependsOnConfig, len(dependServices))
|
||||
}
|
||||
for _, service := range p.Services {
|
||||
if utils.StringContains(dependServices, service.Name) {
|
||||
if utils.StringContains(dependServices, service.Name) &&
|
||||
p.Services[i].DependsOn[service.Name].Condition == "" {
|
||||
p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
}
|
||||
|
||||
@@ -96,6 +96,46 @@ func TestPrepareNetworkLabels(t *testing.T) {
|
||||
}))
|
||||
}
|
||||
|
||||
func TestPrepareVolumes(t *testing.T) {
|
||||
t.Run("adds dependency condition if service depends on volume from another service", func(t *testing.T) {
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
{
|
||||
Name: "aService",
|
||||
VolumesFrom: []string{"anotherService"},
|
||||
},
|
||||
{
|
||||
Name: "anotherService",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := prepareVolumes(&project)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, project.Services[0].DependsOn["anotherService"].Condition, composetypes.ServiceConditionStarted)
|
||||
})
|
||||
t.Run("doesn't overwrite existing dependency condition", func(t *testing.T) {
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
{
|
||||
Name: "aService",
|
||||
VolumesFrom: []string{"anotherService"},
|
||||
DependsOn: map[string]composetypes.ServiceDependency{
|
||||
"anotherService": {Condition: composetypes.ServiceConditionHealthy},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anotherService",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := prepareVolumes(&project)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, project.Services[0].DependsOn["anotherService"].Condition, composetypes.ServiceConditionHealthy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildContainerMountOptions(t *testing.T) {
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
|
||||
@@ -37,80 +37,110 @@ const (
|
||||
ServiceStarted
|
||||
)
|
||||
|
||||
type graphTraversalConfig struct {
|
||||
type graphTraversal struct {
|
||||
mu sync.Mutex
|
||||
seen map[string]struct{}
|
||||
|
||||
extremityNodesFn func(*Graph) []*Vertex // leaves or roots
|
||||
adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren
|
||||
filterAdjacentByStatusFn func(*Graph, string, ServiceStatus) []*Vertex // filterChildren or filterParents
|
||||
targetServiceStatus ServiceStatus
|
||||
adjacentServiceStatusToSkip ServiceStatus
|
||||
|
||||
visitorFn func(context.Context, string) error
|
||||
}
|
||||
|
||||
var (
|
||||
upDirectionTraversalConfig = graphTraversalConfig{
|
||||
func upDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal {
|
||||
return &graphTraversal{
|
||||
extremityNodesFn: leaves,
|
||||
adjacentNodesFn: getParents,
|
||||
filterAdjacentByStatusFn: filterChildren,
|
||||
adjacentServiceStatusToSkip: ServiceStopped,
|
||||
targetServiceStatus: ServiceStarted,
|
||||
visitorFn: visitorFn,
|
||||
}
|
||||
downDirectionTraversalConfig = graphTraversalConfig{
|
||||
}
|
||||
|
||||
func downDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal {
|
||||
return &graphTraversal{
|
||||
extremityNodesFn: roots,
|
||||
adjacentNodesFn: getChildren,
|
||||
filterAdjacentByStatusFn: filterParents,
|
||||
adjacentServiceStatusToSkip: ServiceStarted,
|
||||
targetServiceStatus: ServiceStopped,
|
||||
visitorFn: visitorFn,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// InDependencyOrder applies the function to the services of the project taking in account the dependency order
|
||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
|
||||
return visit(ctx, project, upDirectionTraversalConfig, fn, ServiceStopped)
|
||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
|
||||
graph, err := NewGraph(project.Services, ServiceStopped)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := upDirectionTraversal(fn)
|
||||
return t.visit(ctx, graph)
|
||||
}
|
||||
|
||||
// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
|
||||
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
|
||||
return visit(ctx, project, downDirectionTraversalConfig, fn, ServiceStarted)
|
||||
}
|
||||
|
||||
func visit(ctx context.Context, project *types.Project, traversalConfig graphTraversalConfig, fn func(context.Context, string) error, initialStatus ServiceStatus) error {
|
||||
g := NewGraph(project.Services, initialStatus)
|
||||
if b, err := g.HasCycles(); b {
|
||||
graph, err := NewGraph(project.Services, ServiceStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := downDirectionTraversal(fn)
|
||||
return t.visit(ctx, graph)
|
||||
}
|
||||
|
||||
nodes := traversalConfig.extremityNodesFn(g)
|
||||
func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
|
||||
nodes := t.extremityNodesFn(g)
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
return run(ctx, g, eg, nodes, traversalConfig, fn)
|
||||
})
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
t.run(ctx, g, eg, nodes)
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
// Note: this could be `graph.walk` or whatever
|
||||
func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, traversalConfig graphTraversalConfig, fn func(context.Context, string) error) error {
|
||||
func (t *graphTraversal) run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex) {
|
||||
for _, node := range nodes {
|
||||
// Don't start this service yet if all of its children have
|
||||
// not been started yet.
|
||||
if len(traversalConfig.filterAdjacentByStatusFn(graph, node.Key, traversalConfig.adjacentServiceStatusToSkip)) != 0 {
|
||||
if len(t.filterAdjacentByStatusFn(graph, node.Key, t.adjacentServiceStatusToSkip)) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
node := node
|
||||
if !t.consume(node.Key) {
|
||||
// another worker already visited this node
|
||||
continue
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
err := fn(ctx, node.Service)
|
||||
err := t.visitorFn(ctx, node.Service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
graph.UpdateStatus(node.Key, traversalConfig.targetServiceStatus)
|
||||
graph.UpdateStatus(node.Key, t.targetServiceStatus)
|
||||
|
||||
return run(ctx, graph, eg, traversalConfig.adjacentNodesFn(node), traversalConfig, fn)
|
||||
t.run(ctx, graph, eg, t.adjacentNodesFn(node))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (t *graphTraversal) consume(nodeKey string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.seen == nil {
|
||||
t.seen = make(map[string]struct{})
|
||||
}
|
||||
if _, ok := t.seen[nodeKey]; ok {
|
||||
return false
|
||||
}
|
||||
t.seen[nodeKey] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
// Graph represents project as service dependencies
|
||||
@@ -155,7 +185,7 @@ func (v *Vertex) GetChildren() []*Vertex {
|
||||
}
|
||||
|
||||
// NewGraph returns the dependency graph of the services
|
||||
func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
|
||||
func NewGraph(services types.Services, initialStatus ServiceStatus) (*Graph, error) {
|
||||
graph := &Graph{
|
||||
lock: sync.RWMutex{},
|
||||
Vertices: map[string]*Vertex{},
|
||||
@@ -171,7 +201,11 @@ func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
|
||||
}
|
||||
}
|
||||
|
||||
return graph
|
||||
if b, err := graph.HasCycles(); b {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// NewVertex is the constructor function for the Vertex
|
||||
|
||||
@@ -18,10 +18,13 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
var project = types.Project{
|
||||
@@ -44,6 +47,51 @@ var project = types.Project{
|
||||
},
|
||||
}
|
||||
|
||||
func TestTraversalWithMultipleParents(t *testing.T) {
|
||||
dependent := types.ServiceConfig{
|
||||
Name: "dependent",
|
||||
DependsOn: make(types.DependsOnConfig),
|
||||
}
|
||||
|
||||
project := types.Project{
|
||||
Services: []types.ServiceConfig{dependent},
|
||||
}
|
||||
|
||||
for i := 1; i <= 100; i++ {
|
||||
name := fmt.Sprintf("svc_%d", i)
|
||||
dependent.DependsOn[name] = types.ServiceDependency{}
|
||||
|
||||
svc := types.ServiceConfig{Name: name}
|
||||
project.Services = append(project.Services, svc)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
svc := make(chan string, 10)
|
||||
seen := make(map[string]int)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for service := range svc {
|
||||
seen[service]++
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
err := InDependencyOrder(ctx, &project, func(ctx context.Context, service string) error {
|
||||
svc <- service
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err, "Error during iteration")
|
||||
close(svc)
|
||||
<-done
|
||||
|
||||
testify.Len(t, seen, 101)
|
||||
for svc, count := range seen {
|
||||
assert.Equal(t, 1, count, "Service: %s", svc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInDependencyUpCommandOrder(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
@@ -69,3 +117,181 @@ func TestInDependencyReverseDownCommandOrder(t *testing.T) {
|
||||
require.NoError(t, err, "Error during iteration")
|
||||
require.Equal(t, []string{"test1", "test2", "test3"}, order)
|
||||
}
|
||||
|
||||
func TestBuildGraph(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
services types.Services
|
||||
expectedVertices map[string]*Vertex
|
||||
}{
|
||||
{
|
||||
desc: "builds graph with single service",
|
||||
services: types.Services{
|
||||
{
|
||||
Name: "test",
|
||||
DependsOn: types.DependsOnConfig{},
|
||||
},
|
||||
},
|
||||
expectedVertices: map[string]*Vertex{
|
||||
"test": {
|
||||
Key: "test",
|
||||
Service: "test",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{},
|
||||
Parents: map[string]*Vertex{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "builds graph with two separate services",
|
||||
services: types.Services{
|
||||
{
|
||||
Name: "test",
|
||||
DependsOn: types.DependsOnConfig{},
|
||||
},
|
||||
{
|
||||
Name: "another",
|
||||
DependsOn: types.DependsOnConfig{},
|
||||
},
|
||||
},
|
||||
expectedVertices: map[string]*Vertex{
|
||||
"test": {
|
||||
Key: "test",
|
||||
Service: "test",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{},
|
||||
Parents: map[string]*Vertex{},
|
||||
},
|
||||
"another": {
|
||||
Key: "another",
|
||||
Service: "another",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{},
|
||||
Parents: map[string]*Vertex{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "builds graph with a service and a dependency",
|
||||
services: types.Services{
|
||||
{
|
||||
Name: "test",
|
||||
DependsOn: types.DependsOnConfig{
|
||||
"another": types.ServiceDependency{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "another",
|
||||
DependsOn: types.DependsOnConfig{},
|
||||
},
|
||||
},
|
||||
expectedVertices: map[string]*Vertex{
|
||||
"test": {
|
||||
Key: "test",
|
||||
Service: "test",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{
|
||||
"another": {},
|
||||
},
|
||||
Parents: map[string]*Vertex{},
|
||||
},
|
||||
"another": {
|
||||
Key: "another",
|
||||
Service: "another",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{},
|
||||
Parents: map[string]*Vertex{
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "builds graph with multiple dependency levels",
|
||||
services: types.Services{
|
||||
{
|
||||
Name: "test",
|
||||
DependsOn: types.DependsOnConfig{
|
||||
"another": types.ServiceDependency{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "another",
|
||||
DependsOn: types.DependsOnConfig{
|
||||
"another_dep": types.ServiceDependency{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "another_dep",
|
||||
DependsOn: types.DependsOnConfig{},
|
||||
},
|
||||
},
|
||||
expectedVertices: map[string]*Vertex{
|
||||
"test": {
|
||||
Key: "test",
|
||||
Service: "test",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{
|
||||
"another": {},
|
||||
},
|
||||
Parents: map[string]*Vertex{},
|
||||
},
|
||||
"another": {
|
||||
Key: "another",
|
||||
Service: "another",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{
|
||||
"another_dep": {},
|
||||
},
|
||||
Parents: map[string]*Vertex{
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
"another_dep": {
|
||||
Key: "another_dep",
|
||||
Service: "another_dep",
|
||||
Status: ServiceStopped,
|
||||
Children: map[string]*Vertex{},
|
||||
Parents: map[string]*Vertex{
|
||||
"another": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.desc, func(t *testing.T) {
|
||||
project := types.Project{
|
||||
Services: tC.services,
|
||||
}
|
||||
|
||||
graph, err := NewGraph(project.Services, ServiceStopped)
|
||||
assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
|
||||
|
||||
for k, vertex := range graph.Vertices {
|
||||
expected, ok := tC.expectedVertices[k]
|
||||
assert.Equal(t, true, ok)
|
||||
assert.Equal(t, true, isVertexEqual(*expected, *vertex))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isVertexEqual(a, b Vertex) bool {
|
||||
childrenEquality := true
|
||||
for c := range a.Children {
|
||||
if _, ok := b.Children[c]; !ok {
|
||||
childrenEquality = false
|
||||
}
|
||||
}
|
||||
parentEquality := true
|
||||
for p := range a.Parents {
|
||||
if _, ok := b.Parents[p]; !ok {
|
||||
parentEquality = false
|
||||
}
|
||||
}
|
||||
return a.Key == b.Key &&
|
||||
a.Service == b.Service &&
|
||||
childrenEquality &&
|
||||
parentEquality
|
||||
}
|
||||
|
||||
@@ -86,7 +86,11 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
ops := s.ensureNetworksDown(ctx, project, w)
|
||||
|
||||
if options.Images != "" {
|
||||
ops = append(ops, s.ensureImagesDown(ctx, project, options, w)...)
|
||||
imgOps, err := s.ensureImagesDown(ctx, project, options, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ops = append(ops, imgOps...)
|
||||
}
|
||||
|
||||
if options.Volumes {
|
||||
@@ -118,15 +122,25 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
|
||||
return ops
|
||||
}
|
||||
|
||||
func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) []downOp {
|
||||
func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) ([]downOp, error) {
|
||||
imagePruner := NewImagePruner(s.apiClient(), project)
|
||||
pruneOpts := ImagePruneOptions{
|
||||
Mode: ImagePruneMode(options.Images),
|
||||
RemoveOrphans: options.RemoveOrphans,
|
||||
}
|
||||
images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ops []downOp
|
||||
for image := range s.getServiceImages(options, project) {
|
||||
image := image
|
||||
for i := range images {
|
||||
img := images[i]
|
||||
ops = append(ops, func() error {
|
||||
return s.removeImage(ctx, image, w)
|
||||
return s.removeImage(ctx, img, w)
|
||||
})
|
||||
}
|
||||
return ops
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
|
||||
@@ -190,21 +204,6 @@ func (s *composeService) removeNetwork(ctx context.Context, name string, w progr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) getServiceImages(options api.DownOptions, project *types.Project) map[string]struct{} {
|
||||
images := map[string]struct{}{}
|
||||
for _, service := range project.Services {
|
||||
image := service.Image
|
||||
if options.Images == "local" && image != "" {
|
||||
continue
|
||||
}
|
||||
if image == "" {
|
||||
image = api.GetImageNameOrDefault(service, project.Name)
|
||||
}
|
||||
images[image] = struct{}{}
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
|
||||
id := fmt.Sprintf("Image %s", image)
|
||||
w.Event(progress.NewEvent(id, progress.Working, "Removing"))
|
||||
|
||||
@@ -18,12 +18,15 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/golang/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
@@ -37,7 +40,9 @@ func TestDown(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
@@ -85,7 +90,9 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return(
|
||||
@@ -122,7 +129,9 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
@@ -142,3 +151,152 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestDownRemoveImages(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
opts := compose.DownOptions{
|
||||
Project: &types.Project{
|
||||
Name: strings.ToLower(testProject),
|
||||
Services: types.Services{
|
||||
{Name: "local-anonymous"},
|
||||
{Name: "local-named", Image: "local-named-image"},
|
||||
{Name: "remote", Image: "remote-image"},
|
||||
{Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"},
|
||||
{Name: "no-images-anonymous"},
|
||||
{Name: "no-images-named", Image: "missing-named-image"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).
|
||||
Return([]moby.Container{
|
||||
testContainer("service1", "123", false),
|
||||
}, nil).
|
||||
AnyTimes()
|
||||
|
||||
api.EXPECT().ImageList(gomock.Any(), moby.ImageListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(strings.ToLower(testProject)),
|
||||
filters.Arg("dangling", "false"),
|
||||
),
|
||||
}).Return([]moby.ImageSummary{
|
||||
{
|
||||
Labels: types.Labels{compose.ServiceLabel: "local-anonymous"},
|
||||
RepoTags: []string{"testproject-local-anonymous:latest"},
|
||||
},
|
||||
{
|
||||
Labels: types.Labels{compose.ServiceLabel: "local-named"},
|
||||
RepoTags: []string{"local-named-image:latest"},
|
||||
},
|
||||
}, nil).AnyTimes()
|
||||
|
||||
imagesToBeInspected := map[string]bool{
|
||||
"testproject-local-anonymous": true,
|
||||
"local-named-image": true,
|
||||
"remote-image": true,
|
||||
"testproject-no-images-anonymous": false,
|
||||
"missing-named-image": false,
|
||||
}
|
||||
for img, exists := range imagesToBeInspected {
|
||||
var resp moby.ImageInspect
|
||||
var err error
|
||||
if exists {
|
||||
resp.RepoTags = []string{img}
|
||||
} else {
|
||||
err = errdefs.NotFound(fmt.Errorf("test specified that image %q should not exist", img))
|
||||
}
|
||||
|
||||
api.EXPECT().ImageInspectWithRaw(gomock.Any(), img).
|
||||
Return(resp, nil, err).
|
||||
AnyTimes()
|
||||
}
|
||||
|
||||
api.EXPECT().ImageInspectWithRaw(gomock.Any(), "registry.example.com/remote-image-tagged:v1.0").
|
||||
Return(moby.ImageInspect{RepoTags: []string{"registry.example.com/remote-image-tagged:v1.0"}}, nil, nil).
|
||||
AnyTimes()
|
||||
|
||||
localImagesToBeRemoved := []string{
|
||||
"testproject-local-anonymous:latest",
|
||||
}
|
||||
for _, img := range localImagesToBeRemoved {
|
||||
// test calls down --rmi=local then down --rmi=all, so local images
|
||||
// get "removed" 2x, while other images are only 1x
|
||||
api.EXPECT().ImageRemove(gomock.Any(), img, moby.ImageRemoveOptions{}).
|
||||
Return(nil, nil).
|
||||
Times(2)
|
||||
}
|
||||
|
||||
t.Log("-> docker compose down --rmi=local")
|
||||
opts.Images = "local"
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), opts)
|
||||
assert.NilError(t, err)
|
||||
|
||||
otherImagesToBeRemoved := []string{
|
||||
"local-named-image:latest",
|
||||
"remote-image:latest",
|
||||
"registry.example.com/remote-image-tagged:v1.0",
|
||||
}
|
||||
for _, img := range otherImagesToBeRemoved {
|
||||
api.EXPECT().ImageRemove(gomock.Any(), img, moby.ImageRemoveOptions{}).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
t.Log("-> docker compose down --rmi=all")
|
||||
opts.Images = "all"
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), opts)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestDownRemoveImages_NoLabel(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
container := testContainer("service1", "123", false)
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]moby.Container{container}, nil)
|
||||
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{
|
||||
Volumes: []*moby.Volume{{Name: "myProject_volume"}},
|
||||
}, nil)
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return(nil, nil)
|
||||
|
||||
// ImageList returns no images for the project since they were unlabeled
|
||||
// (created by an older version of Compose)
|
||||
api.EXPECT().ImageList(gomock.Any(), moby.ImageListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(strings.ToLower(testProject)),
|
||||
filters.Arg("dangling", "false"),
|
||||
),
|
||||
}).Return(nil, nil)
|
||||
|
||||
api.EXPECT().ImageInspectWithRaw(gomock.Any(), "testproject-service1").
|
||||
Return(moby.ImageInspect{}, nil, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true}).Return(nil)
|
||||
|
||||
api.EXPECT().ImageRemove(gomock.Any(), "testproject-service1:latest", moby.ImageRemoveOptions{}).Return(nil, nil)
|
||||
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
254
pkg/compose/image_pruner.go
Normal file
254
pkg/compose/image_pruner.go
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/distribution/distribution/v3/reference"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
// ImagePruneMode controls how aggressively images associated with the project
|
||||
// are removed from the engine.
|
||||
type ImagePruneMode string
|
||||
|
||||
const (
|
||||
// ImagePruneNone indicates that no project images should be removed.
|
||||
ImagePruneNone ImagePruneMode = ""
|
||||
// ImagePruneLocal indicates that only images built locally by Compose
|
||||
// should be removed.
|
||||
ImagePruneLocal ImagePruneMode = "local"
|
||||
// ImagePruneAll indicates that all project-associated images, including
|
||||
// remote images should be removed.
|
||||
ImagePruneAll ImagePruneMode = "all"
|
||||
)
|
||||
|
||||
// ImagePruneOptions controls the behavior of image pruning.
|
||||
type ImagePruneOptions struct {
|
||||
Mode ImagePruneMode
|
||||
|
||||
// RemoveOrphans will result in the removal of images that were built for
|
||||
// the project regardless of whether they are for a known service if true.
|
||||
RemoveOrphans bool
|
||||
}
|
||||
|
||||
// ImagePruner handles image removal during Compose `down` operations.
|
||||
type ImagePruner struct {
|
||||
client client.ImageAPIClient
|
||||
project *types.Project
|
||||
}
|
||||
|
||||
// NewImagePruner creates an ImagePruner object for a project.
|
||||
func NewImagePruner(imageClient client.ImageAPIClient, project *types.Project) *ImagePruner {
|
||||
return &ImagePruner{
|
||||
client: imageClient,
|
||||
project: project,
|
||||
}
|
||||
}
|
||||
|
||||
// ImagesToPrune returns the set of images that should be removed.
|
||||
func (p *ImagePruner) ImagesToPrune(ctx context.Context, opts ImagePruneOptions) ([]string, error) {
|
||||
if opts.Mode == ImagePruneNone {
|
||||
return nil, nil
|
||||
} else if opts.Mode != ImagePruneLocal && opts.Mode != ImagePruneAll {
|
||||
return nil, fmt.Errorf("unsupported image prune mode: %s", opts.Mode)
|
||||
}
|
||||
var images []string
|
||||
|
||||
if opts.Mode == ImagePruneAll {
|
||||
namedImages, err := p.namedImages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
images = append(images, namedImages...)
|
||||
}
|
||||
|
||||
projectImages, err := p.labeledLocalImages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, img := range projectImages {
|
||||
if len(img.RepoTags) == 0 {
|
||||
// currently, we're only pruning the tagged references, but
|
||||
// if we start removing the dangling images and grouping by
|
||||
// service, we can remove this (and should rely on `Image::ID`)
|
||||
continue
|
||||
}
|
||||
|
||||
var shouldPrune bool
|
||||
if opts.RemoveOrphans {
|
||||
// indiscriminately prune all project images even if they're not
|
||||
// referenced by the current Compose state (e.g. the service was
|
||||
// removed from YAML)
|
||||
shouldPrune = true
|
||||
} else {
|
||||
// only prune the image if it belongs to a known service for the
|
||||
// project AND is either an implicitly-named, locally-built image
|
||||
// or `--rmi=all` has been specified.
|
||||
// TODO(milas): now that Compose labels the images it builds, this
|
||||
// makes less sense; arguably, locally-built but explicitly-named
|
||||
// images should be removed with `--rmi=local` as well.
|
||||
service, err := p.project.GetService(img.Labels[api.ServiceLabel])
|
||||
if err == nil && (opts.Mode == ImagePruneAll || service.Image == "") {
|
||||
shouldPrune = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldPrune {
|
||||
images = append(images, img.RepoTags[0])
|
||||
}
|
||||
}
|
||||
|
||||
fallbackImages, err := p.unlabeledLocalImages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
images = append(images, fallbackImages...)
|
||||
|
||||
images = normalizeAndDedupeImages(images)
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// namedImages are those that are explicitly named in the service config.
|
||||
//
|
||||
// These could be registry-only images (no local build), hybrid (support build
|
||||
// as a fallback if cannot pull), or local-only (image does not exist in a
|
||||
// registry).
|
||||
func (p *ImagePruner) namedImages(ctx context.Context) ([]string, error) {
|
||||
var images []string
|
||||
for _, service := range p.project.Services {
|
||||
if service.Image == "" {
|
||||
continue
|
||||
}
|
||||
images = append(images, service.Image)
|
||||
}
|
||||
return p.filterImagesByExistence(ctx, images)
|
||||
}
|
||||
|
||||
// labeledLocalImages are images that were locally-built by a current version of
|
||||
// Compose (it did not always label built images).
|
||||
//
|
||||
// The image name could either have been defined by the user or implicitly
|
||||
// created from the project + service name.
|
||||
func (p *ImagePruner) labeledLocalImages(ctx context.Context) ([]moby.ImageSummary, error) {
|
||||
imageListOpts := moby.ImageListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(p.project.Name),
|
||||
// TODO(milas): we should really clean up the dangling images as
|
||||
// well (historically we have NOT); need to refactor this to handle
|
||||
// it gracefully without producing confusing CLI output, i.e. we
|
||||
// do not want to print out a bunch of untagged/dangling image IDs,
|
||||
// they should be grouped into a logical operation for the relevant
|
||||
// service
|
||||
filters.Arg("dangling", "false"),
|
||||
),
|
||||
}
|
||||
projectImages, err := p.client.ImageList(ctx, imageListOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return projectImages, nil
|
||||
}
|
||||
|
||||
// unlabeledLocalImages are images that match the implicit naming convention
|
||||
// for locally-built images but did not get labeled, presumably because they
|
||||
// were produced by an older version of Compose.
|
||||
//
|
||||
// This is transitional to ensure `down` continues to work as expected on
|
||||
// projects built/launched by previous versions of Compose. It can safely
|
||||
// be removed after some time.
|
||||
func (p *ImagePruner) unlabeledLocalImages(ctx context.Context) ([]string, error) {
|
||||
var images []string
|
||||
for _, service := range p.project.Services {
|
||||
if service.Image != "" {
|
||||
continue
|
||||
}
|
||||
img := api.GetImageNameOrDefault(service, p.project.Name)
|
||||
images = append(images, img)
|
||||
}
|
||||
return p.filterImagesByExistence(ctx, images)
|
||||
}
|
||||
|
||||
// filterImagesByExistence returns the subset of images that exist in the
|
||||
// engine store.
|
||||
//
|
||||
// NOTE: Any transient errors communicating with the API will result in an
|
||||
// image being returned as "existing", as this method is exclusively used to
|
||||
// find images to remove, so the worst case of being conservative here is an
|
||||
// attempt to remove an image that doesn't exist, which will cause a warning
|
||||
// but is otherwise harmless.
|
||||
func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []string) ([]string, error) {
|
||||
var mu sync.Mutex
|
||||
var ret []string
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, img := range imageNames {
|
||||
img := img
|
||||
eg.Go(func() error {
|
||||
_, _, err := p.client.ImageInspectWithRaw(ctx, img)
|
||||
if errdefs.IsNotFound(err) {
|
||||
// err on the side of caution: only skip if we successfully
|
||||
// queried the API and got back a definitive "not exists"
|
||||
return nil
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
ret = append(ret, img)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// normalizeAndDedupeImages returns the unique set of images after normalization.
|
||||
func normalizeAndDedupeImages(images []string) []string {
|
||||
seen := make(map[string]struct{}, len(images))
|
||||
for _, img := range images {
|
||||
// since some references come from user input (service.image) and some
|
||||
// come from the engine API, we standardize them, opting for the
|
||||
// familiar name format since they'll also be displayed in the CLI
|
||||
ref, err := reference.ParseNormalizedNamed(img)
|
||||
if err == nil {
|
||||
ref = reference.TagNameOnly(ref)
|
||||
img = reference.FamiliarString(ref)
|
||||
}
|
||||
seen[img] = struct{}{}
|
||||
}
|
||||
ret := make([]string, 0, len(seen))
|
||||
for v := range seen {
|
||||
ret = append(ret, v)
|
||||
}
|
||||
// ensure a deterministic return result - the actual ordering is not useful
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
||||
@@ -35,15 +35,15 @@ import (
|
||||
|
||||
const testProject = "testProject"
|
||||
|
||||
var tested = composeService{}
|
||||
|
||||
func TestKillAll(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
@@ -74,7 +74,9 @@ func TestKillSignal(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
@@ -97,9 +99,13 @@ func TestKillSignal(t *testing.T) {
|
||||
}
|
||||
|
||||
func testContainer(service string, id string, oneOff bool) moby.Container {
|
||||
// canonical docker names in the API start with a leading slash, some
|
||||
// parts of Compose code will attempt to strip this off, so make sure
|
||||
// it's consistently present
|
||||
name := "/" + strings.TrimPrefix(id, "/")
|
||||
return moby.Container{
|
||||
ID: id,
|
||||
Names: []string{id},
|
||||
Names: []string{name},
|
||||
Labels: containerLabels(service, oneOff),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,32 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error {
|
||||
func (s *composeService) Logs(
|
||||
ctx context.Context,
|
||||
projectName string,
|
||||
consumer api.LogConsumer,
|
||||
options api.LogOptions,
|
||||
) error {
|
||||
projectName = strings.ToLower(projectName)
|
||||
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, options.Services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project := options.Project
|
||||
if project == nil {
|
||||
project, err = s.getProjectWithResources(ctx, containers, projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.Services) == 0 {
|
||||
options.Services = project.ServiceNames()
|
||||
}
|
||||
|
||||
containers = containers.filter(isService(options.Services...))
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range containers {
|
||||
c := c
|
||||
|
||||
204
pkg/compose/logs_test.go
Normal file
204
pkg/compose/logs_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
compose "github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, moby.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name)),
|
||||
}).Return(
|
||||
[]moby.Container{
|
||||
testContainer("service", "c", false),
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
api.EXPECT().
|
||||
ContainerInspect(anyCancellableContext(), "c").
|
||||
Return(moby.ContainerJSON{
|
||||
ContainerJSONBase: &moby.ContainerJSONBase{ID: "c"},
|
||||
Config: &container.Config{Tty: false},
|
||||
}, nil)
|
||||
c1Reader, c1Writer := io.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = c1Reader.Close()
|
||||
_ = c1Writer.Close()
|
||||
})
|
||||
c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout)
|
||||
c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr)
|
||||
go func() {
|
||||
_, err := c1Stdout.Write([]byte("hello stdout\n"))
|
||||
assert.NoError(t, err, "Writing to fake stdout")
|
||||
_, err = c1Stderr.Write([]byte("hello stderr\n"))
|
||||
assert.NoError(t, err, "Writing to fake stderr")
|
||||
_ = c1Writer.Close()
|
||||
}()
|
||||
api.EXPECT().ContainerLogs(anyCancellableContext(), "c", gomock.Any()).
|
||||
Return(c1Reader, nil)
|
||||
|
||||
opts := compose.LogOptions{
|
||||
Project: &types.Project{
|
||||
Services: types.Services{
|
||||
{Name: "service"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
consumer := &testLogConsumer{}
|
||||
err := tested.Logs(ctx, name, consumer, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
[]string{"hello stdout", "hello stderr"},
|
||||
consumer.LogsForContainer("service", "c"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestComposeService_Logs_ServiceFiltering ensures that we do not include
|
||||
// logs from out-of-scope services based on the Compose file vs actual state.
|
||||
//
|
||||
// NOTE(milas): This test exists because each method is currently duplicating
|
||||
// a lot of the project/service filtering logic. We should consider moving it
|
||||
// to an earlier point in the loading process, at which point this test could
|
||||
// safely be removed.
|
||||
func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, moby.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name)),
|
||||
}).Return(
|
||||
[]moby.Container{
|
||||
testContainer("serviceA", "c1", false),
|
||||
testContainer("serviceA", "c2", false),
|
||||
// serviceB will be filtered out by the project definition to
|
||||
// ensure we ignore "orphan" containers
|
||||
testContainer("serviceB", "c3", false),
|
||||
testContainer("serviceC", "c4", false),
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
for _, id := range []string{"c1", "c2", "c4"} {
|
||||
id := id
|
||||
api.EXPECT().
|
||||
ContainerInspect(anyCancellableContext(), id).
|
||||
Return(
|
||||
moby.ContainerJSON{
|
||||
ContainerJSONBase: &moby.ContainerJSONBase{ID: id},
|
||||
Config: &container.Config{Tty: true},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
api.EXPECT().ContainerLogs(anyCancellableContext(), id, gomock.Any()).
|
||||
Return(io.NopCloser(strings.NewReader("hello "+id+"\n")), nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
// this simulates passing `--filename` with a Compose file that does NOT
|
||||
// reference `serviceB` even though it has running services for this proj
|
||||
proj := &types.Project{
|
||||
Services: types.Services{
|
||||
{Name: "serviceA"},
|
||||
{Name: "serviceC"},
|
||||
},
|
||||
}
|
||||
consumer := &testLogConsumer{}
|
||||
opts := compose.LogOptions{
|
||||
Project: proj,
|
||||
}
|
||||
err := tested.Logs(ctx, name, consumer, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("serviceA", "c1"))
|
||||
require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("serviceA", "c2"))
|
||||
require.Empty(t, consumer.LogsForContainer("serviceB", "c3"))
|
||||
require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("serviceC", "c4"))
|
||||
}
|
||||
|
||||
type testLogConsumer struct {
|
||||
mu sync.Mutex
|
||||
// logs is keyed by service, then container; values are log lines
|
||||
logs map[string]map[string][]string
|
||||
}
|
||||
|
||||
func (l *testLogConsumer) Log(containerName, service, message string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.logs == nil {
|
||||
l.logs = make(map[string]map[string][]string)
|
||||
}
|
||||
if l.logs[service] == nil {
|
||||
l.logs[service] = make(map[string][]string)
|
||||
}
|
||||
l.logs[service][containerName] = append(l.logs[service][containerName], message)
|
||||
}
|
||||
|
||||
func (l *testLogConsumer) Status(containerName, msg string) {}
|
||||
|
||||
func (l *testLogConsumer) Register(containerName string) {}
|
||||
|
||||
func (l *testLogConsumer) LogsForContainer(svc string, containerName string) []string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.logs[svc][containerName]
|
||||
}
|
||||
@@ -93,11 +93,13 @@ func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if exitCodeFrom == "" {
|
||||
exitCodeFrom = event.Service
|
||||
}
|
||||
if exitCodeFrom == event.Service {
|
||||
exitCode = event.ExitCode
|
||||
if event.Type == api.ContainerEventExit {
|
||||
if exitCodeFrom == "" {
|
||||
exitCodeFrom = event.Service
|
||||
}
|
||||
if exitCodeFrom == event.Service {
|
||||
exitCode = event.ExitCode
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
|
||||
@@ -38,7 +38,9 @@ func TestPs(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -181,6 +181,18 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
||||
RegistryAuth: base64.URLEncoding.EncodeToString(buf),
|
||||
Platform: service.Platform,
|
||||
})
|
||||
|
||||
// check if has error and the service has a build section
|
||||
// then the status should be warning instead of error
|
||||
if err != nil && service.Build != nil {
|
||||
w.Event(progress.Event{
|
||||
ID: service.Name,
|
||||
Status: progress.Warning,
|
||||
Text: "Warning",
|
||||
})
|
||||
return "", WrapCategorisedComposeError(err, PullFailure)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Event(progress.Event{
|
||||
ID: service.Name,
|
||||
|
||||
@@ -112,6 +112,9 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
|
||||
}
|
||||
if opts.Entrypoint != nil {
|
||||
service.Entrypoint = opts.Entrypoint
|
||||
if len(opts.Command) == 0 {
|
||||
service.Command = []string{}
|
||||
}
|
||||
}
|
||||
if len(opts.Environment) > 0 {
|
||||
cmdEnv := types.NewMappingWithEquals(opts.Environment)
|
||||
|
||||
@@ -50,6 +50,13 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.Services) > 0 {
|
||||
err := project.ForServices(options.Services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
if listener != nil {
|
||||
attached, err := s.attach(ctx, project, listener, options.AttachTo)
|
||||
|
||||
@@ -38,7 +38,9 @@ func TestStopTimeout(t *testing.T) {
|
||||
|
||||
api := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested.dockerCli = cli
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
cli.EXPECT().Client().Return(api).AnyTimes()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -60,7 +60,7 @@ func (l *lockedBuffer) RequireEventuallyContains(t testing.TB, v string) {
|
||||
"Error: %v", err)
|
||||
}
|
||||
return strings.Contains(bufContents.String(), v)
|
||||
}, 2*time.Second, 20*time.Millisecond,
|
||||
}, 5*time.Second, 20*time.Millisecond,
|
||||
"Buffer did not contain %q\n============\n%s\n============",
|
||||
v, &bufContents)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ package e2e
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
@@ -84,6 +86,51 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{Out: `"RESULT": "SUCCESS"`})
|
||||
})
|
||||
|
||||
t.Run("build as part of up", func(t *testing.T) {
|
||||
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
|
||||
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
|
||||
})
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
|
||||
output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
|
||||
|
||||
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
|
||||
c.RunDockerCmd(t, "image", "inspect", "custom-nginx")
|
||||
})
|
||||
|
||||
t.Run("no rebuild when up again", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("rebuild when up --build", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "up", "-d", "--build")
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
})
|
||||
|
||||
t.Run("cleanup build project", func(t *testing.T) {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
|
||||
c.RunDockerCmd(t, "rmi", "build-test-nginx")
|
||||
c.RunDockerCmd(t, "rmi", "custom-nginx")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildSSH(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Running on Windows. Skipping...")
|
||||
}
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Run("build failed with ssh default value", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--ssh", "")
|
||||
res.Assert(t, icmd.Expected{
|
||||
@@ -129,47 +176,12 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
})
|
||||
c.RunDockerCmd(t, "image", "inspect", "build-test-ssh")
|
||||
})
|
||||
|
||||
t.Run("build as part of up", func(t *testing.T) {
|
||||
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
|
||||
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
|
||||
})
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
|
||||
output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
|
||||
|
||||
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
|
||||
c.RunDockerCmd(t, "image", "inspect", "custom-nginx")
|
||||
})
|
||||
|
||||
t.Run("no rebuild when up again", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
|
||||
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("rebuild when up --build", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "up", "-d", "--build")
|
||||
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
})
|
||||
|
||||
t.Run("cleanup build project", func(t *testing.T) {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
|
||||
c.RunDockerCmd(t, "rmi", "build-test-nginx")
|
||||
c.RunDockerCmd(t, "rmi", "custom-nginx")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildSecrets(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on windows")
|
||||
}
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Run("build with secrets", func(t *testing.T) {
|
||||
@@ -211,6 +223,10 @@ func TestBuildImageDependencies(t *testing.T) {
|
||||
doTest := func(t *testing.T, cli *CLI) {
|
||||
resetState := func() {
|
||||
cli.RunDockerComposeCmd(t, "down", "--rmi=all", "-t=0")
|
||||
res := cli.RunDockerOrExitError(t, "image", "rm", "build-dependencies-service")
|
||||
if res.Error != nil {
|
||||
require.Contains(t, res.Stderr(), `Error: No such image: build-dependencies-service`)
|
||||
}
|
||||
}
|
||||
resetState()
|
||||
t.Cleanup(resetState)
|
||||
@@ -229,6 +245,15 @@ func TestBuildImageDependencies(t *testing.T) {
|
||||
"image", "inspect", "--format={{ index .RepoTags 0 }}",
|
||||
"build-dependencies-service")
|
||||
res.Assert(t, icmd.Expected{Out: "build-dependencies-service:latest"})
|
||||
|
||||
res = cli.RunDockerComposeCmd(t, "down", "-t0", "--rmi=all", "--remove-orphans")
|
||||
t.Log(res.Combined())
|
||||
|
||||
res = cli.RunDockerOrExitError(t, "image", "inspect", "build-dependencies-service")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: "Error: No such image: build-dependencies-service",
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ClassicBuilder", func(t *testing.T) {
|
||||
@@ -243,3 +268,116 @@ func TestBuildImageDependencies(t *testing.T) {
|
||||
t.Skip("See https://github.com/docker/compose/issues/9232")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Running on Windows. Skipping...")
|
||||
}
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
// declare builder
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform")
|
||||
})
|
||||
|
||||
t.Run("platform not supported by builder", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
|
||||
"-f", "fixtures/build-test/platforms/compose-unsupported-platform.yml", "build")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 17,
|
||||
Err: "failed to solve: alpine: no match for platform in",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("multi-arch build ok", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
res.Assert(t, icmd.Expected{Out: "I am building for linux/arm64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
|
||||
|
||||
})
|
||||
|
||||
t.Run("multi-arch multi service builds ok", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
|
||||
"-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build")
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/arm64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/amd64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/arm64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/amd64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/arm64"})
|
||||
res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/amd64"})
|
||||
})
|
||||
|
||||
t.Run("multi-arch up --build", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build")
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
res.Assert(t, icmd.Expected{Out: "platforms-platforms-1 exited with code 0"})
|
||||
})
|
||||
|
||||
t.Run("use DOCKER_DEFAULT_PLATFORM value when up --build", func(t *testing.T) {
|
||||
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build")
|
||||
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
|
||||
cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=linux/amd64")
|
||||
})
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "I am building for linux/arm64"))
|
||||
})
|
||||
|
||||
t.Run("use service platform value when no build platforms defined ", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
|
||||
"-f", "fixtures/build-test/platforms/compose-service-platform-and-no-build-platforms.yaml", "build")
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
res.Assert(t, icmd.Expected{Out: "I am building for linux/386"})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestBuildPlatformsStandardErrors(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Run("no platform support with Classic Builder", func(t *testing.T) {
|
||||
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
|
||||
|
||||
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
|
||||
cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
|
||||
})
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: "this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("builder does not support multi-arch", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 17,
|
||||
Err: `multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("service platform not defined in platforms build section", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
|
||||
"-f", "fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml", "build")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: `service.platform "linux/riscv64" should be part of the service.build.platforms: ["linux/amd64" "linux/arm64"]`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("DOCKER_DEFAULT_PLATFORM value not defined in platforms build section", func(t *testing.T) {
|
||||
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
|
||||
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
|
||||
cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=windows/amd64")
|
||||
})
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: `DOCKER_DEFAULT_PLATFORM "windows/amd64" value should be part of the service.build.platforms: ["linux/amd64" "linux/arm64"]`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@ func TestDownComposefileInParentFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAttachRestart(t *testing.T) {
|
||||
if _, ok := os.LookupEnv("CI"); ok {
|
||||
t.Skip("Skipping test on CI... flaky")
|
||||
}
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
cmd := c.NewDockerComposeCmd(t, "--ansi=never", "--project-directory", "./fixtures/attach-restart", "up")
|
||||
@@ -146,7 +149,7 @@ func TestAttachRestart(t *testing.T) {
|
||||
return strings.Count(res.Stdout(),
|
||||
"failing-1 exited with code 1") == 3, fmt.Sprintf("'failing-1 exited with code 1' not found 3 times in : \n%s\n",
|
||||
debug)
|
||||
}, 2*time.Minute, 2*time.Second)
|
||||
}, 4*time.Minute, 2*time.Second)
|
||||
|
||||
assert.Equal(t, strings.Count(res.Stdout(), "failing-1 | world"), 3, res.Combined())
|
||||
}
|
||||
@@ -234,3 +237,25 @@ networks:
|
||||
name: compose-e2e-convert_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertInterpolate(t *testing.T) {
|
||||
const projectName = "compose-e2e-convert-interpolate"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
|
||||
t.Run("convert", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "convert", "--no-interpolate")
|
||||
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services:
|
||||
nginx:
|
||||
build:
|
||||
context: %s
|
||||
dockerfile: ${MYVAR}
|
||||
networks:
|
||||
default: null
|
||||
networks:
|
||||
default:
|
||||
name: compose-e2e-convert-interpolate_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
func TestUpWait(t *testing.T) {
|
||||
@@ -45,3 +46,13 @@ func TestUpWait(t *testing.T) {
|
||||
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
}
|
||||
|
||||
func TestUpExitCodeFrom(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
const projectName = "e2e-exit-code-from"
|
||||
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--exit-code-from=test")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 137})
|
||||
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
|
||||
}
|
||||
|
||||
22
pkg/e2e/fixtures/build-test/platforms/Dockerfile
Normal file
22
pkg/e2e/fixtures/build-test/platforms/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
RUN echo "I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
|
||||
|
||||
FROM alpine
|
||||
COPY --from=build /log /log
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
serviceA:
|
||||
image: build-test-platform-a:test
|
||||
build:
|
||||
context: ./contextServiceA
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
serviceB:
|
||||
image: build-test-platform-b:test
|
||||
build:
|
||||
context: ./contextServiceB
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
serviceC:
|
||||
image: build-test-platform-c:test
|
||||
build:
|
||||
context: ./contextServiceC
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
platforms:
|
||||
image: build-test-platform:test
|
||||
platform: linux/386
|
||||
build:
|
||||
context: .
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
platforms:
|
||||
image: build-test-platform:test
|
||||
platform: linux/riscv64
|
||||
build:
|
||||
context: .
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
platforms:
|
||||
image: build-test-platform:test
|
||||
build:
|
||||
context: .
|
||||
platforms:
|
||||
- unsupported/unsupported
|
||||
- linux/amd64
|
||||
9
pkg/e2e/fixtures/build-test/platforms/compose.yaml
Normal file
9
pkg/e2e/fixtures/build-test/platforms/compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
platforms:
|
||||
image: build-test-platform:test
|
||||
build:
|
||||
context: .
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
RUN echo "I'm Service A and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
|
||||
|
||||
FROM alpine
|
||||
COPY --from=build /log /log
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
RUN echo "I'm Service B and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
|
||||
|
||||
FROM alpine
|
||||
COPY --from=build /log /log
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
RUN echo "I'm Service C and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
|
||||
|
||||
FROM alpine
|
||||
COPY --from=build /log /log
|
||||
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
nginx:
|
||||
build:
|
||||
context: nginx-build
|
||||
dockerfile: ${MYVAR}
|
||||
11
pkg/e2e/fixtures/start-fail/start-depends_on-long-lived.yaml
Normal file
11
pkg/e2e/fixtures/start-fail/start-depends_on-long-lived.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
safe:
|
||||
image: 'alpine'
|
||||
command: ['/bin/sh', '-c', 'sleep infinity'] # never exiting
|
||||
failure:
|
||||
image: 'alpine'
|
||||
command: ['/bin/sh', '-c', 'sleep 2 ; echo "exiting" ; exit 42']
|
||||
test:
|
||||
image: 'alpine'
|
||||
command: ['/bin/sh', '-c', 'sleep 99999 ; echo "tests are OK"'] # very long job
|
||||
depends_on: [safe]
|
||||
17
pkg/e2e/fixtures/start-stop/start-stop-deps.yaml
Normal file
17
pkg/e2e/fixtures/start-stop/start-stop-deps.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
another_2:
|
||||
image: nginx:alpine
|
||||
another:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- another_2
|
||||
dep_2:
|
||||
image: nginx:alpine
|
||||
dep_1:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- dep_2
|
||||
desired:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- dep_1
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +30,9 @@ import (
|
||||
)
|
||||
|
||||
func TestPause(t *testing.T) {
|
||||
if _, ok := os.LookupEnv("CI"); ok {
|
||||
t.Skip("Skipping test on CI... flaky")
|
||||
}
|
||||
cli := NewParallelCLI(t, WithEnv(
|
||||
"COMPOSE_PROJECT_NAME=e2e-pause",
|
||||
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))
|
||||
@@ -46,7 +50,7 @@ func TestPause(t *testing.T) {
|
||||
"b": urlForService(t, cli, "b", 80),
|
||||
}
|
||||
for _, url := range urls {
|
||||
HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 5*time.Second)
|
||||
HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 20*time.Second)
|
||||
}
|
||||
|
||||
// pause a and verify that it can no longer be hit but b still can
|
||||
@@ -98,7 +102,7 @@ func TestPauseServiceAlreadyPaused(t *testing.T) {
|
||||
|
||||
// launch a and wait for it to come up
|
||||
cli.RunDockerComposeCmd(t, "up", "-d", "a")
|
||||
HTTPGetWithRetry(t, urlForService(t, cli, "a", 80), http.StatusOK, 50*time.Millisecond, 5*time.Second)
|
||||
HTTPGetWithRetry(t, urlForService(t, cli, "a", 80), http.StatusOK, 50*time.Millisecond, 10*time.Second)
|
||||
|
||||
// pause a twice - first time should pass, second time fail
|
||||
cli.RunDockerComposeCmd(t, "pause", "a")
|
||||
|
||||
@@ -247,6 +247,30 @@ func TestStartStopMultipleServices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartSingleServiceAndDependency(t *testing.T) {
|
||||
cli := NewParallelCLI(t, WithEnv(
|
||||
"COMPOSE_PROJECT_NAME=e2e-start-single-deps",
|
||||
"COMPOSE_FILE=./fixtures/start-stop/start-stop-deps.yaml"))
|
||||
t.Cleanup(func() {
|
||||
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
|
||||
})
|
||||
|
||||
cli.RunDockerComposeCmd(t, "create", "desired")
|
||||
|
||||
res := cli.RunDockerComposeCmd(t, "start", "desired")
|
||||
desiredServices := []string{"desired", "dep_1", "dep_2"}
|
||||
for _, s := range desiredServices {
|
||||
startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s)
|
||||
assert.Assert(t, strings.Contains(res.Combined(), startMsg),
|
||||
fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined()))
|
||||
}
|
||||
undesiredServices := []string{"another", "another_2"}
|
||||
for _, s := range undesiredServices {
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), s),
|
||||
fmt.Sprintf("Shouldn't have message for service: %s\n%s", s, res.Combined()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartStopMultipleFiles(t *testing.T) {
|
||||
cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple-files"))
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright 2022 Docker Compose CLI authors
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -99,6 +100,9 @@ func TestProjectVolumeBind(t *testing.T) {
|
||||
const projectName = "compose-e2e-project-volume-bind"
|
||||
|
||||
t.Run("up on project volume with bind specification", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Running on Windows. Skipping...")
|
||||
}
|
||||
tmpDir, err := os.MkdirTemp("", projectName)
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpDir) //nolint
|
||||
|
||||
@@ -28,6 +28,8 @@ const (
|
||||
Done
|
||||
// Error means that the current task has errored
|
||||
Error
|
||||
// Warning means that the current task has warning
|
||||
Warning
|
||||
)
|
||||
|
||||
// Event represents a progress event.
|
||||
|
||||
@@ -75,7 +75,7 @@ func (w *ttyWriter) Event(e Event) {
|
||||
if _, ok := w.events[e.ID]; ok {
|
||||
last := w.events[e.ID]
|
||||
switch e.Status {
|
||||
case Done, Error:
|
||||
case Done, Error, Warning:
|
||||
if last.Status != e.Status {
|
||||
last.stop()
|
||||
}
|
||||
@@ -222,6 +222,9 @@ func lineText(event Event, pad string, terminalWidth, statusPadding int, color b
|
||||
if event.Status == Error {
|
||||
color = aec.RedF
|
||||
}
|
||||
if event.Status == Warning {
|
||||
color = aec.YellowF
|
||||
}
|
||||
return aec.Apply(o, color)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ func TestLineText(t *testing.T) {
|
||||
ev.Status = Error
|
||||
out = lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m")
|
||||
|
||||
ev.Status = Warning
|
||||
out = lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[33m . id Text Status 0.0s\n\x1b[0m")
|
||||
}
|
||||
|
||||
func TestLineTextSingleEvent(t *testing.T) {
|
||||
@@ -103,3 +107,32 @@ func TestErrorEvent(t *testing.T) {
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second)))
|
||||
}
|
||||
|
||||
func TestWarningEvent(t *testing.T) {
|
||||
w := &ttyWriter{
|
||||
events: map[string]Event{},
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
e := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Working",
|
||||
startTime: time.Now(),
|
||||
spinner: &spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
// Fire "Working" event and check end time isn't touched
|
||||
w.Event(e)
|
||||
event, ok := w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.Equal(time.Time{}))
|
||||
|
||||
// Fire "Warning" event and check end time is set
|
||||
e.Status = Warning
|
||||
w.Event(e)
|
||||
event, ok = w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second)))
|
||||
}
|
||||
|
||||
30
pkg/utils/slices.go
Normal file
30
pkg/utils/slices.go
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 utils
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Contains helps to detect if a non-comparable struct is part of an array
|
||||
// only use this method if you can't rely on existing golang Contains function of slices (https://pkg.go.dev/golang.org/x/exp/slices#Contains)
|
||||
func Contains[T any](origin []T, element T) bool {
|
||||
for _, v := range origin {
|
||||
if reflect.DeepEqual(v, element) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
95
pkg/utils/slices_test.go
Normal file
95
pkg/utils/slices_test.go
Normal file
@@ -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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
source := []specs.Platform{
|
||||
{
|
||||
Architecture: "linux/amd64",
|
||||
OS: "darwin",
|
||||
OSVersion: "",
|
||||
OSFeatures: nil,
|
||||
Variant: "",
|
||||
},
|
||||
{
|
||||
Architecture: "linux/arm64",
|
||||
OS: "linux",
|
||||
OSVersion: "12",
|
||||
OSFeatures: nil,
|
||||
Variant: "v8",
|
||||
},
|
||||
{
|
||||
Architecture: "",
|
||||
OS: "",
|
||||
OSVersion: "",
|
||||
OSFeatures: nil,
|
||||
Variant: "",
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
origin []specs.Platform
|
||||
element specs.Platform
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "element found",
|
||||
args: args{
|
||||
origin: source,
|
||||
element: specs.Platform{
|
||||
Architecture: "linux/arm64",
|
||||
OS: "linux",
|
||||
OSVersion: "12",
|
||||
OSFeatures: nil,
|
||||
Variant: "v8",
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "element not found",
|
||||
args: args{
|
||||
origin: source,
|
||||
element: specs.Platform{
|
||||
Architecture: "linux/arm64",
|
||||
OS: "darwin",
|
||||
OSVersion: "12",
|
||||
OSFeatures: nil,
|
||||
Variant: "v8",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Contains(tt.args.origin, tt.args.element); got != tt.want {
|
||||
t.Errorf("Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user