Compare commits

..

6 Commits

Author SHA1 Message Date
Nicolas De Loof
f06caeb844 oras doesn't prepend index.docker.io to repository ref
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-07 09:26:13 +02:00
Nicolas De Loof
49d1bc7524 introduce push --repository
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-05 09:06:41 +02:00
Guillaume Lours
827e864ed0 Merge pull request #10745 from glours/add-builder-support
add support of --builder and BUILDX_BUILDER
2023-07-03 10:46:02 +02:00
Guillaume Lours
28301fb1a4 add support of --builder and BUILDX_BUILDER
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-07-03 10:11:18 +02:00
Ulysses Souza
fa3e16c66b Merge pull request #10742 from ulyssessouza/add-wait
Add `docker compose wait`
2023-07-02 13:54:34 +02:00
Ulysses Souza
edd76bfd70 Add docker compose wait
Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
2023-06-30 16:07:03 +02:00
24 changed files with 688 additions and 11 deletions

View File

@@ -43,6 +43,7 @@ type buildOptions struct {
noCache bool
memory cliopts.MemBytes
ssh string
builder string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
@@ -54,6 +55,10 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
return api.BuildOptions{}, err
}
}
builderName := opts.builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
}
return api.BuildOptions{
Pull: opts.pull,
@@ -64,6 +69,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
Quiet: opts.quiet,
Services: services,
SSHs: SSHKeys,
Builder: builderName,
}, nil
}
@@ -101,6 +107,7 @@ func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cob
cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.")
cmd.Flags().StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
cmd.Flags().StringVar(&opts.builder, "builder", "", "Set builder to use.")
cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED")
cmd.Flags().MarkHidden("parallel") //nolint:errcheck
cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED")

View File

@@ -427,10 +427,12 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
imagesCommand(&opts, streams, backend),
versionCommand(streams),
buildCommand(&opts, &progress, backend),
publishCommand(&opts, backend),
pushCommand(&opts, backend),
pullCommand(&opts, backend),
createCommand(&opts, backend),
copyCommand(&opts, backend),
waitCommand(&opts, backend),
alphaCommand(&opts, backend),
)

55
cmd/compose/publish.go Normal file
View File

@@ -0,0 +1,55 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type publishOptions struct {
*ProjectOptions
composeOptions
Repository string
}
func publishCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
publishCmd := &cobra.Command{
Use: "publish [OPTIONS] [REPOSITORY]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, backend, opts, args[0])
}),
Args: cobra.ExactArgs(1),
}
return publishCmd
}
func runPublish(ctx context.Context, backend api.Service, opts pushOptions, repository string) error {
project, err := opts.ToProject(nil)
if err != nil {
return err
}
return backend.Publish(ctx, project, repository)
}

View File

@@ -31,6 +31,7 @@ type pushOptions struct {
IncludeDeps bool
Ignorefailures bool
Quiet bool
Repository string
}
func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
@@ -48,6 +49,7 @@ func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
pushCmd.Flags().StringVarP(&opts.Repository, "repository", "r", "", "Also publish the compose application in repository")
return pushCmd
}
@@ -68,5 +70,6 @@ func runPush(ctx context.Context, backend api.Service, opts pushOptions, service
return backend.Push(ctx, project, api.PushOptions{
IgnoreFailures: opts.Ignorefailures,
Quiet: opts.Quiet,
Repository: opts.Repository,
})
}

72
cmd/compose/wait.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type waitOptions struct {
*ProjectOptions
services []string
downProject bool
}
func waitCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
var statusCode int64
var err error
cmd := &cobra.Command{
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
Short: "Block until the first service container stops",
Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services
statusCode, err = runWait(ctx, backend, &opts)
return err
}),
PostRun: func(cmd *cobra.Command, args []string) {
os.Exit(int(statusCode))
},
}
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
return cmd
}
func runWait(ctx context.Context, backend api.Service, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName()
if err != nil {
return 0, err
}
return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,
})
}

View File

@@ -33,6 +33,7 @@ Define and run multi-container applications with Docker.
| [`unpause`](compose_unpause.md) | Unpause services |
| [`up`](compose_up.md) | Create and start containers |
| [`version`](compose_version.md) | Show the Docker Compose version information |
| [`wait`](compose_wait.md) | Block until the first service container stops |
### Options

View File

@@ -8,6 +8,7 @@ Build or rebuild services
| Name | Type | Default | Description |
|:-----------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
| `--build-arg` | `stringArray` | | Set build-time variables for services. |
| `--builder` | `string` | | Set builder to use. |
| `--dry-run` | | | Execute command in dry run mode |
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
| `--no-cache` | | | Do not use cache when building the image |

View File

@@ -0,0 +1,15 @@
# docker compose wait
<!---MARKER_GEN_START-->
Block until the first service container stops
### Options
| Name | Type | Default | Description |
|:-----------------|:-----|:--------|:---------------------------------------------|
| `--down-project` | | | Drops project when the first container stops |
| `--dry-run` | | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@@ -171,6 +171,7 @@ cname:
- docker compose unpause
- docker compose up
- docker compose version
- docker compose wait
clink:
- docker_compose_build.yaml
- docker_compose_config.yaml
@@ -197,6 +198,7 @@ clink:
- docker_compose_unpause.yaml
- docker_compose_up.yaml
- docker_compose_version.yaml
- docker_compose_wait.yaml
options:
- option: ansi
value_type: string

View File

@@ -24,6 +24,15 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: builder
value_type: string
description: Set builder to use.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compress
value_type: bool
default_value: "true"

View File

@@ -0,0 +1,34 @@
command: docker compose wait
short: Block until the first service container stops
long: Block until the first service container stops
usage: docker compose wait SERVICE [SERVICE...] [OPTIONS]
pname: docker compose
plink: docker_compose.yaml
options:
- option: down-project
value_type: bool
default_value: "false"
description: Drops project when the first container stops
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

3
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/moby/term v0.5.0
github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc3
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
@@ -183,6 +183,7 @@ require (
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
oras.land/oras-go/v2 v2.2.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect

4
go.sum
View File

@@ -513,6 +513,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
@@ -1110,6 +1112,8 @@ k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+O
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo=
oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -74,6 +74,8 @@ type Service interface {
Events(ctx context.Context, projectName string, options EventsOptions) error
// Port executes the equivalent to a `compose port`
Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error)
// Publish executes the equivalent to a `compose publish`
Publish(ctx context.Context, project *types.Project, repository string) error
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
// MaxConcurrency defines upper limit for concurrent operations against engine API
@@ -84,6 +86,15 @@ type Service interface {
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
// Viz generates a graphviz graph of the project services
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
// Wait blocks until at least one of the services' container exits
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
}
type WaitOptions struct {
// Services passed in the command line to be waited
Services []string
// Executes a down when a container exits
DownProjectOnContainerExit bool
}
type VizOptions struct {
@@ -121,6 +132,8 @@ type BuildOptions struct {
SSHs []types.SSHKey
// Memory limit for the build container
Memory int64
// Builder name passed in the command line
Builder string
}
// Apply mutates project according to build options
@@ -249,6 +262,7 @@ type ConfigOptions struct {
// PushOptions group options of the Push API
type PushOptions struct {
Quiet bool
Repository string
IgnoreFailures bool
}

View File

@@ -54,6 +54,8 @@ type ServiceProxy struct {
MaxConcurrencyFn func(parallel int)
DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error)
VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error)
PublishFn func(ctx context.Context, project *types.Project, repository string) error
interceptors []Interceptor
}
@@ -90,11 +92,13 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
s.TopFn = service.Top
s.EventsFn = service.Events
s.PortFn = service.Port
s.PublishFn = service.Publish
s.ImagesFn = service.Images
s.WatchFn = service.Watch
s.MaxConcurrencyFn = service.MaxConcurrency
s.DryRunModeFn = service.DryRunMode
s.VizFn = service.Viz
s.WaitFn = service.Wait
return s
}
@@ -309,6 +313,10 @@ func (s *ServiceProxy) Port(ctx context.Context, projectName string, service str
return s.PortFn(ctx, projectName, service, port, options)
}
func (s *ServiceProxy) Publish(ctx context.Context, project *types.Project, repository string) error {
return s.PublishFn(ctx, project, repository)
}
// Images implements Service interface
func (s *ServiceProxy) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
if s.ImagesFn == nil {
@@ -325,7 +333,7 @@ func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, servic
return s.WatchFn(ctx, project, services, options)
}
// Viz implements Viz interface
// Viz implements Service interface
func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error) {
if s.VizFn == nil {
return "", ErrNotImplemented
@@ -333,6 +341,14 @@ func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options
return s.VizFn(ctx, project, options)
}
// Wait implements Service interface
func (s *ServiceProxy) Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error) {
if s.WaitFn == nil {
return 0, ErrNotImplemented
}
return s.WaitFn(ctx, projectName, options)
}
func (s *ServiceProxy) MaxConcurrency(i int) {
s.MaxConcurrencyFn(i)
}

View File

@@ -116,7 +116,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args))
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w)
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, options.Builder)
if err != nil {
return err
}

View File

@@ -34,8 +34,8 @@ import (
"github.com/moby/buildkit/client"
)
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer) (string, error) {
b, err := builder.New(s.dockerCli)
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer, builderName string) (string, error) {
b, err := builder.New(s.dockerCli, builder.WithName(builderName))
if err != nil {
return "", err
}

185
pkg/compose/publish.go Normal file
View File

@@ -0,0 +1,185 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
client2 "github.com/docker/cli/cli/registry/client"
"github.com/docker/compose/v2/pkg/api"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string) error {
err := s.Push(ctx, project, api.PushOptions{})
if err != nil {
return err
}
target, err := reference.ParseDockerRef(repository)
if err != nil {
return err
}
client := s.dockerCli.RegistryClient(false)
for i, service := range project.Services {
ref, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}
auth, err := encodedAuth(ref, s.configFile())
if err != nil {
return err
}
inspect, err := s.apiClient().DistributionInspect(ctx, ref.String(), auth)
if err != nil {
return err
}
canonical, err := reference.WithDigest(ref, inspect.Descriptor.Digest)
if err != nil {
return err
}
to, err := reference.WithDigest(target, inspect.Descriptor.Digest)
if err != nil {
return err
}
err = client.MountBlob(ctx, canonical, to)
switch err.(type) {
case client2.ErrBlobCreated:
default:
return err
}
service.Image = to.String()
project.Services[i] = service
}
err = s.publishComposeYaml(ctx, project, repository)
if err != nil {
return err
}
return nil
}
func (s *composeService) publishComposeYaml(ctx context.Context, project *types.Project, repository string) error {
ref, err := reference.ParseDockerRef(repository)
if err != nil {
return err
}
var manifests []v1.Descriptor
for _, composeFile := range project.ComposeFiles {
stat, err := os.Stat(composeFile)
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, "oras", "push", "--artifact-type", "application/vnd.docker.compose.yaml", ref.String(), composeFile)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = s.stderr()
err = cmd.Start()
if err != nil {
return err
}
out, err := io.ReadAll(stdout)
if err != nil {
return err
}
var composeFileDigest string
for _, line := range strings.Split(string(out), "\n") {
if strings.HasPrefix(line, "Digest: ") {
composeFileDigest = line[len("Digest: "):]
}
fmt.Fprintln(s.stdout(), line)
}
if composeFileDigest == "" {
return fmt.Errorf("expected oras to display `Digest: xxx`")
}
err = cmd.Wait()
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest.Digest(composeFileDigest),
Size: stat.Size(),
ArtifactType: "application/vnd.docker.compose.yaml",
})
}
for _, service := range project.Services {
dockerRef, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
Digest: dockerRef.(reference.Digested).Digest(),
Annotations: map[string]string{
"com.docker.compose.service": service.Name,
},
})
}
manifest := v1.Index{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Annotations: map[string]string{
"com.docker.compose": api.ComposeVersion,
},
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
return err
}
temp, err := os.CreateTemp(os.TempDir(), "compose")
if err != nil {
return err
}
err = os.WriteFile(temp.Name(), manifestContent, 0o700)
if err != nil {
return err
}
defer os.Remove(temp.Name())
cmd := exec.CommandContext(ctx, "oras", "manifest", "push", ref.String(), temp.Name())
cmd.Stdout = s.stdout()
cmd.Stderr = s.stderr()
err = cmd.Run()
if err != nil {
return err
}
return nil
}

View File

@@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -26,14 +27,17 @@ import (
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
"github.com/docker/buildx/driver"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry/remote"
)
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
@@ -45,8 +49,8 @@ func (s *composeService) Push(ctx context.Context, project *types.Project, optio
}, s.stdinfo(), "Pushing")
}
func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(ctx)
func (s *composeService) push(upctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(upctx)
eg.SetLimit(s.maxConcurrency)
info, err := s.apiClient().Info(ctx)
@@ -79,7 +83,66 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
return nil
})
}
return eg.Wait()
err = eg.Wait()
if err != nil {
return err
}
ctx = upctx
if options.Repository != "" {
repository, err := remote.NewRepository(options.Repository)
if err != nil {
return err
}
yaml, err := project.MarshalYAML()
if err != nil {
return err
}
manifests := []v1.Descriptor{
{
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Digest: digest.FromBytes(yaml),
Size: int64(len(yaml)),
Data: yaml,
ArtifactType: "application/vnd.docker.compose.yaml",
},
}
for _, service := range project.Services {
inspected, _, err := s.dockerCli.Client().ImageInspectWithRaw(ctx, service.Image)
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
Digest: digest.Digest(inspected.RepoDigests[0]),
Size: inspected.Size,
Annotations: map[string]string{
"com.docker.compose.service": service.Name,
},
})
}
manifest := v1.Index{
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Annotations: map[string]string{
"com.docker.compose": api.ComposeVersion,
},
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
return err
}
manifestDescriptor := content.NewDescriptorFromBytes(v1.MediaTypeImageIndex, manifestContent)
err = repository.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
if err != nil {
return err
}
}
return nil
}
func (s *composeService) pushServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {

67
pkg/compose/wait.go Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...)
if err != nil {
return 0, err
}
if len(containers) == 0 {
return 0, fmt.Errorf("no containers for project %q", projectName)
}
eg, waitCtx := errgroup.WithContext(ctx)
var statusCode int64
for _, c := range containers {
c := c
eg.Go(func() error {
var err error
resultC, errC := s.dockerCli.Client().ContainerWait(waitCtx, c.ID, "")
select {
case result := <-resultC:
fmt.Fprintf(s.dockerCli.Out(), "container %q exited with status code %d\n", c.ID, result.StatusCode)
statusCode = result.StatusCode
case err = <-errC:
}
return err
})
}
err = eg.Wait()
if err != nil {
return 42, err // Ignore abort flag in case of error in wait
}
if options.DownProjectOnContainerExit {
return statusCode, s.Down(ctx, projectName, api.DownOptions{
RemoveOrphans: true,
})
}
return statusCode, err
}

View File

@@ -17,6 +17,7 @@
package e2e
import (
"fmt"
"net/http"
"runtime"
"strings"
@@ -423,3 +424,30 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
})
}
func TestBuildBuilder(t *testing.T) {
c := NewParallelCLI(t)
builderName := "build-with-builder"
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("use specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", builderName)
assert.NilError(t, res.Error, res.Stderr())
})
t.Run("error when using specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", "unknown-builder")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: fmt.Sprintf(`no builder %q found`, "unknown-builder"),
})
})
}

View File

@@ -0,0 +1,11 @@
services:
faster:
image: alpine
command: sleep 2
slower:
image: alpine
command: sleep 5
infinity:
image: alpine
command: sleep infinity

72
pkg/e2e/wait_test.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
}
func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
}
func TestWaitOnInfinity(t *testing.T) {
const projectName = "e2e-wait-infinity"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
finished := make(chan struct{})
ticker := time.NewTicker(7 * time.Second)
go func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
finished <- struct{}{}
}()
select {
case <-finished:
t.Fatal("wait infinity should not finish")
case <-ticker.C:
}
}
func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
}

View File

@@ -423,6 +423,21 @@ func (mr *MockServiceMockRecorder) Viz(ctx, project, options interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockService)(nil).Viz), ctx, project, options)
}
// Wait mocks base method.
func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait", ctx, projectName, options)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Wait indicates an expected call of Wait.
func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockService)(nil).Wait), ctx, projectName, options)
}
// Watch mocks base method.
func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
m.ctrl.T.Helper()