Merge remote-tracking branch 'upstream/v2' into go-1.19.1

This commit is contained in:
Milas Bowman 2022-09-13 14:46:15 +01:00
commit 42169db166
42 changed files with 1349 additions and 100 deletions

View File

@ -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

View File

@ -25,6 +25,7 @@ import (
"strings"
"syscall"
cnabgodocker "github.com/cnabio/cnab-go/driver/docker"
"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types"
composegoutils "github.com/compose-spec/compose-go/utils"
@ -32,6 +33,7 @@ import (
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/client"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -291,6 +293,16 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
if err != nil {
return err
}
// Reset DockerCli and APIClient to get possible `DOCKER_HOST` and/or `DOCKER_CONTEXT` loaded from environment file.
err = dockerCli.Apply(func(cli *command.DockerCli) error {
return cli.Initialize(cnabgodocker.BuildDockerClientOptions(),
command.WithInitializeClient(func(_ *command.DockerCli) (client.APIClient, error) {
return nil, nil
}))
})
if err != nil {
return err
}
parent := cmd.Root()
if parent != nil {
parentPrerun := parent.PersistentPreRunE

View File

@ -18,6 +18,7 @@ package compose
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
@ -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)
}

View File

@ -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,

View File

@ -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,
})
}

35
cmd/compose/tracing.go Normal file
View 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) {}

22
go.mod
View File

@ -3,8 +3,9 @@ module github.com/docker/compose/v2
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-go v0.24.1-0.20220907172316-1ca5c8721bf7
github.com/cnabio/cnab-to-oci v0.3.7
github.com/compose-spec/compose-go v1.5.0
github.com/containerd/console v1.0.3
@ -44,7 +45,6 @@ require (
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cnabio/cnab-go v0.23.4 // indirect
github.com/containerd/continuity v0.2.3-0.20220330195504-d132b287edc8 // indirect
github.com/containerd/ttrpc v1.1.0 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
@ -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,12 +101,12 @@ 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-20220906165146-f3363e06e74c // 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,19 @@ 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/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // 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 (

30
go.sum
View File

@ -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=
@ -268,8 +269,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
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-go v0.24.1-0.20220907172316-1ca5c8721bf7 h1:6cETeoyahKaH4hNShuB4KUqkTdjLVKEpTakHW5bpDW8=
github.com/cnabio/cnab-go v0.24.1-0.20220907172316-1ca5c8721bf7/go.mod h1:Zm0HTH8xxzinB64SXm7KFSna7DEN0ZjZwrRwZpfgChU=
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=
@ -498,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=
@ -556,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=
@ -761,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=
@ -998,6 +1002,8 @@ github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
@ -1016,12 +1022,15 @@ github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ=
github.com/moby/buildkit v0.10.1-0.20220403220257-10e6f94bf90d/go.mod h1:WvwAZv8aRScHkqc/+X46cRC2CKMKpqcaX+pRvUTtPes=
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=
@ -1236,6 +1245,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=
@ -1461,18 +1471,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=
@ -1489,8 +1503,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=
@ -1499,6 +1514,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=

View File

@ -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

View File

@ -81,6 +81,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
Attrs: map[string]string{"ref": image},
})
}
if len(buildOptions.Platforms) > 1 {
buildOptions.Exports = []bclient.ExportEntry{{
Type: "image",
Attrs: map[string]string{},
}}
}
opts[imageName] = buildOptions
}
@ -162,6 +168,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 = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil {
opt.Platforms = []specs.Platform{}
}
opts[imageName] = opt
continue
}
@ -206,7 +221,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) {
@ -215,20 +230,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)
@ -352,3 +356,40 @@ 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 := useDockerDefaultPlatform(project, service.Build.Platforms)
if err != nil {
return nil, err
}
if service.Platform != "" && !utils.StringContains(service.Build.Platforms, service.Platform) {
return nil, fmt.Errorf("service.platform should be part of the service.build.platforms: %q", service.Platform)
}
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 useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
var plats []specs.Platform
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
if !utils.StringContains(platformList, platform) {
return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM value should be part of the service.build.platforms: %q", platform)
}
p, err := platforms.Parse(platform)
if err != nil {
return nil, err
}
plats = append(plats, p)
}
return plats, nil
}

View File

@ -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)
}

View File

@ -89,6 +89,10 @@ 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")
}
switch {
case isLocalDir(specifiedContext):
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName)

View File

@ -17,7 +17,6 @@
package compose
import (
"bytes"
"context"
"encoding/json"
"fmt"
@ -95,28 +94,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{

View File

@ -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) {

View File

@ -63,21 +63,24 @@ var (
)
// 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(*graphTraversalConfig)) error {
graph, err := NewGraph(project.Services, ServiceStopped)
if err != nil {
return err
}
return visit(ctx, graph, upDirectionTraversalConfig, fn)
}
// 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
}
return visit(ctx, graph, downDirectionTraversalConfig, fn)
}
func visit(ctx context.Context, g *Graph, traversalConfig graphTraversalConfig, fn func(context.Context, string) error) error {
nodes := traversalConfig.extremityNodesFn(g)
eg, _ := errgroup.WithContext(ctx)
@ -155,7 +158,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 +174,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

View File

@ -18,10 +18,12 @@ package compose
import (
"context"
"fmt"
"testing"
"github.com/compose-spec/compose-go/types"
"github.com/stretchr/testify/require"
"gotest.tools/assert"
)
var project = types.Project{
@ -69,3 +71,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
}

View File

@ -37,7 +37,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 +87,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 +126,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(
@ -149,7 +155,9 @@ func TestDownRemoveImageLocal(t *testing.T) {
api := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
tested := composeService{
dockerCli: cli,
}
cli.EXPECT().Client().Return(api).AnyTimes()
container := testContainer("service1", "123", false)
@ -180,7 +188,9 @@ func TestDownRemoveImageLocalNoLabel(t *testing.T) {
api := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
tested := composeService{
dockerCli: cli,
}
cli.EXPECT().Client().Return(api).AnyTimes()
container := testContainer("service1", "123", false)
@ -208,7 +218,9 @@ func TestDownRemoveImageAll(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(

View File

@ -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),
}
}

View File

@ -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
View 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]
}

View File

@ -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()

View File

@ -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,

View File

@ -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)

View File

@ -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()

View File

@ -243,3 +243,105 @@ func TestBuildImageDependencies(t *testing.T) {
t.Skip("See https://github.com/docker/compose/issues/9232")
})
}
func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
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"))
})
}
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 should be part of the service.build.platforms: "linux/riscv64"`,
})
})
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 value should be part of the service.build.platforms: "windows/amd64"`,
})
})
}

View File

@ -234,3 +234,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})
})
}

View 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

View File

@ -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

View File

@ -0,0 +1,9 @@
services:
platforms:
image: build-test-platform:test
platform: linux/riscv64
build:
context: .
platforms:
- linux/amd64
- linux/arm64

View File

@ -0,0 +1,8 @@
services:
platforms:
image: build-test-platform:test
build:
context: .
platforms:
- unsupported/unsupported
- linux/amd64

View File

@ -0,0 +1,9 @@
services:
platforms:
image: build-test-platform:test
build:
context: .
platforms:
- linux/amd64
- linux/arm64

View 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'm Service A and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log

View 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'm Service B and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log

View 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'm Service C and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log

View File

@ -0,0 +1,5 @@
services:
nginx:
build:
context: nginx-build
dockerfile: ${MYVAR}

View 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

View File

@ -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() {

View File

@ -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.

View File

@ -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)
}

View File

@ -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
View 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
View 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)
}
})
}
}