mirror of
https://github.com/docker/compose.git
synced 2026-02-13 20:19:29 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bfbc147a | ||
|
|
fbbd6f83d7 | ||
|
|
a000978980 | ||
|
|
361c0893a9 | ||
|
|
513b6128c2 | ||
|
|
eececb9add | ||
|
|
501b5acde6 | ||
|
|
f51bc4cd00 | ||
|
|
517f87a372 | ||
|
|
718049cbd7 | ||
|
|
02371f3127 | ||
|
|
a7c9de82b2 | ||
|
|
51ebeb5441 | ||
|
|
fafaa9c5b8 | ||
|
|
fc9c3cde06 | ||
|
|
73bfbab54b | ||
|
|
2ac081b4c4 | ||
|
|
eeea049f17 | ||
|
|
26064d4b60 | ||
|
|
7c46beb8af | ||
|
|
aa1ec4524c | ||
|
|
a4ee6ca7a5 | ||
|
|
5617eff0c2 | ||
|
|
fa24ab8e25 | ||
|
|
0aad9595a5 | ||
|
|
813900180e | ||
|
|
82417bd5bc | ||
|
|
0cbb73c024 | ||
|
|
38e3d670a9 | ||
|
|
24c78728e0 | ||
|
|
9eeb2d3154 | ||
|
|
8da82c98ef | ||
|
|
1a8c855489 | ||
|
|
15bd0b0c5f | ||
|
|
39d0f6477e | ||
|
|
3a95a0872d | ||
|
|
f794c79eb3 | ||
|
|
407d825705 | ||
|
|
82b41b9ebd | ||
|
|
6c06170eb0 | ||
|
|
60c1311f67 | ||
|
|
17add87e4e | ||
|
|
bf0418bac1 | ||
|
|
b9d0c77cde | ||
|
|
bdb8545611 | ||
|
|
41df35c1f4 | ||
|
|
3ef5045a08 | ||
|
|
d9df7aab6e | ||
|
|
c9d96b449b | ||
|
|
1744b45765 | ||
|
|
87f457e7dc | ||
|
|
abcc91e2b9 | ||
|
|
8b9fe89842 | ||
|
|
34b18194f7 | ||
|
|
ce27dba52e | ||
|
|
d2b9456137 | ||
|
|
9c60fe67df |
44
.github/SECURITY.md
vendored
Normal file
44
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Security Policy
|
||||
|
||||
The maintainers of Docker Compose take security seriously. If you discover
|
||||
a security issue, please bring it to their attention right away!
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately
|
||||
to [security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
|
||||
received.
|
||||
|
||||
## Review Process
|
||||
|
||||
After receiving the report, an initial triage and technical analysis is
|
||||
performed to confirm the report and determine its scope. We may request
|
||||
additional information in this stage of the process.
|
||||
|
||||
Once a reviewer has confirmed the relevance of the report, a draft security
|
||||
advisory will be created on GitHub. The draft advisory will be used to discuss
|
||||
the issue with maintainers, the reporter(s), and where applicable, other
|
||||
affected parties under embargo.
|
||||
|
||||
If the vulnerability is accepted, a timeline for developing a patch, public
|
||||
disclosure, and patch release will be determined. If there is an embargo period
|
||||
on public disclosure before the patch release, the reporter(s) are expected to
|
||||
participate in the discussion of the timeline and abide by agreed upon dates
|
||||
for public disclosure.
|
||||
|
||||
## Accreditation
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you,
|
||||
although we will keep your name confidential if you request it. We also like to
|
||||
send gifts - if you're into swag, make sure to let us know. We do not currently
|
||||
offer a paid security bounty program at this time.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
This project docs not provide long-term supported versions, and only the current
|
||||
release and `main` branch are actively maintained. Docker Compose v1, and the
|
||||
corresponding [v1 branch](https://github.com/docker/compose/tree/v1) reached
|
||||
EOL and are no longer supported.
|
||||
|
||||
9
.github/workflows/docs-upstream.yml
vendored
9
.github/workflows/docs-upstream.yml
vendored
@@ -2,6 +2,15 @@
|
||||
# to check if yaml reference docs used in this repo are valid
|
||||
name: docs-upstream
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
10
.github/workflows/stale.yml
vendored
10
.github/workflows/stale.yml
vendored
@@ -1,4 +1,14 @@
|
||||
name: 'Close stale issues'
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG GO_VERSION=1.22.8
|
||||
ARG XX_VERSION=1.2.1
|
||||
ARG GOLANGCI_LINT_VERSION=v1.60.2
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
people = [
|
||||
"glours",
|
||||
"jhrotko",
|
||||
"milas",
|
||||
"ndeloof",
|
||||
"nicksieger",
|
||||
@@ -72,6 +73,11 @@
|
||||
Email = "guillaume.tardif@docker.com"
|
||||
GitHub = "gtardif"
|
||||
|
||||
[people.jhrotko]
|
||||
Name = "Joana Hrotko"
|
||||
Email = "joana.hrotko@docker.com"
|
||||
Github = "jhrotko"
|
||||
|
||||
[people.laurazard]
|
||||
Name = "Laura Brehm"
|
||||
Email = "laura.brehm@docker.com"
|
||||
|
||||
@@ -33,6 +33,7 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
cmd.AddCommand(
|
||||
vizCommand(p, dockerCli, backend),
|
||||
publishCommand(p, dockerCli, backend),
|
||||
generateCommand(p, backend),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/pkg/kvfile"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/desktop"
|
||||
"github.com/docker/compose/v2/internal/experimental"
|
||||
@@ -70,6 +72,26 @@ const (
|
||||
ComposeMenu = "COMPOSE_MENU"
|
||||
)
|
||||
|
||||
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
|
||||
func rawEnv(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error) {
|
||||
lines, err := kvfile.ParseFromReader(r, lookup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse env_file %s: %w", filename, err)
|
||||
}
|
||||
vars := types.Mapping{}
|
||||
for _, line := range lines {
|
||||
key, value, _ := strings.Cut(line, "=")
|
||||
vars[key] = value
|
||||
}
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// compose evaluates env file values for interpolation
|
||||
// `raw` format allows to load env_file with the same parser used by docker run --env-file
|
||||
dotenv.RegisterFormat("raw", rawEnv)
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
api.Service
|
||||
|
||||
@@ -580,7 +602,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
}
|
||||
|
||||
c.AddCommand(
|
||||
upCommand(&opts, dockerCli, backend, experiments),
|
||||
upCommand(&opts, dockerCli, backend),
|
||||
downCommand(&opts, dockerCli, backend),
|
||||
startCommand(&opts, dockerCli, backend),
|
||||
restartCommand(&opts, dockerCli, backend),
|
||||
@@ -594,6 +616,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
removeCommand(&opts, dockerCli, backend),
|
||||
execCommand(&opts, dockerCli, backend),
|
||||
attachCommand(&opts, dockerCli, backend),
|
||||
exportCommand(&opts, dockerCli, backend),
|
||||
pauseCommand(&opts, dockerCli, backend),
|
||||
unpauseCommand(&opts, dockerCli, backend),
|
||||
topCommand(&opts, dockerCli, backend),
|
||||
|
||||
@@ -66,9 +66,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
|
||||
flags := copyCmd.Flags()
|
||||
flags.IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
|
||||
flags.BoolVar(&opts.all, "all", false, "Copy to all the containers of the service")
|
||||
flags.MarkHidden("all") //nolint:errcheck
|
||||
flags.MarkDeprecated("all", "By default all the containers of the service will get the source file/directory to be copied") //nolint:errcheck
|
||||
flags.BoolVar(&opts.all, "all", false, "Include containers created by the run command")
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
|
||||
|
||||
74
cmd/compose/export.go
Normal file
74
cmd/compose/export.go
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type exportOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
service string
|
||||
output string
|
||||
index int
|
||||
}
|
||||
|
||||
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := exportOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "export [OPTIONS] SERVICE",
|
||||
Short: "Export a service container's filesystem as a tar archive",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
options.service = args[0]
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runExport(ctx, dockerCli, backend, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
|
||||
flags.StringVarP(&options.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
|
||||
projectName, err := options.toProjectName(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exportOptions := api.ExportOptions{
|
||||
Service: options.service,
|
||||
Index: options.index,
|
||||
Output: options.output,
|
||||
}
|
||||
|
||||
return backend.Export(ctx, projectName, exportOptions)
|
||||
}
|
||||
82
cmd/compose/generate.go
Normal file
82
cmd/compose/generate.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type generateOptions struct {
|
||||
*ProjectOptions
|
||||
Format string
|
||||
}
|
||||
|
||||
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := generateOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate [OPTIONS] [CONTAINERS...]",
|
||||
Short: "EXPERIMENTAL - Generate a Compose file from existing containers",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runGenerate(ctx, backend, opts, args)
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&opts.ProjectName, "name", "", "Project name to set in the Compose file")
|
||||
cmd.Flags().StringVar(&opts.ProjectDir, "project-dir", "", "Directory to use for the project")
|
||||
cmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
|
||||
if len(containers) == 0 {
|
||||
return fmt.Errorf("at least one container must be specified")
|
||||
}
|
||||
project, err := backend.Generate(ctx, api.GenerateOptions{
|
||||
Containers: containers,
|
||||
ProjectName: opts.ProjectName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var content []byte
|
||||
switch opts.Format {
|
||||
case "json":
|
||||
content, err = project.MarshalJSON()
|
||||
case "yaml":
|
||||
content, err = project.MarshalYAML()
|
||||
default:
|
||||
return fmt.Errorf("unsupported format %q", opts.Format)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(content))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "publish [OPTIONS] [REPOSITORY]",
|
||||
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
|
||||
Short: "Publish compose application",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPublish(ctx, dockerCli, backend, opts, args[0])
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/experimental"
|
||||
xprogress "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -81,13 +80,19 @@ func (opts upOptions) apply(project *types.Project, services []string) (*types.P
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli, experimentals *experimental.State) {
|
||||
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) {
|
||||
if !dockerCli.Out().IsTerminal() {
|
||||
opts.navigationMenu = false
|
||||
return
|
||||
}
|
||||
// If --menu flag was not set
|
||||
if !opts.navigationMenuChanged {
|
||||
opts.navigationMenu = SetUnchangedOption(ComposeMenu, experimentals.NavBar())
|
||||
if envVar, ok := os.LookupEnv(ComposeMenu); ok {
|
||||
opts.navigationMenu = utils.StringToBool(envVar)
|
||||
return
|
||||
}
|
||||
// ...and COMPOSE_MENU env var is not defined we want the default value to be true
|
||||
opts.navigationMenu = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +107,7 @@ func (opts upOptions) OnExit() api.Cascade {
|
||||
}
|
||||
}
|
||||
|
||||
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, experiments *experimental.State) *cobra.Command {
|
||||
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
up := upOptions{}
|
||||
create := createOptions{}
|
||||
build := buildOptions{ProjectOptions: p}
|
||||
@@ -127,7 +132,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
|
||||
return errors.New("cannot combine --attach and --attach-dependencies")
|
||||
}
|
||||
|
||||
up.validateNavigationMenu(dockerCli, experiments)
|
||||
up.validateNavigationMenu(dockerCli)
|
||||
|
||||
if !p.All && len(project.Services) == 0 {
|
||||
return fmt.Errorf("no service selected")
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/cli/cli/command"
|
||||
)
|
||||
|
||||
var names = []string{
|
||||
@@ -59,7 +59,7 @@ const (
|
||||
)
|
||||
|
||||
// SetANSIMode configure formatter for colored output on ANSI-compliant console
|
||||
func SetANSIMode(streams api.Streams, ansi string) {
|
||||
func SetANSIMode(streams command.Streams, ansi string) {
|
||||
if !useAnsi(streams, ansi) {
|
||||
nextColor = func() colorFunc {
|
||||
return monochrome
|
||||
@@ -68,7 +68,7 @@ func SetANSIMode(streams api.Streams, ansi string) {
|
||||
}
|
||||
}
|
||||
|
||||
func useAnsi(streams api.Streams, ansi string) bool {
|
||||
func useAnsi(streams command.Streams, ansi string) bool {
|
||||
switch ansi {
|
||||
case Always:
|
||||
return true
|
||||
|
||||
@@ -61,21 +61,32 @@ func (l *logConsumer) Register(name string) {
|
||||
}
|
||||
|
||||
func (l *logConsumer) register(name string) *presenter {
|
||||
cf := monochrome
|
||||
if l.color {
|
||||
if name == api.WatchLogger {
|
||||
cf = makeColorFunc("92")
|
||||
} else {
|
||||
cf = nextColor()
|
||||
var p *presenter
|
||||
root, _, found := strings.Cut(name, " ")
|
||||
if found {
|
||||
parent := l.getPresenter(root)
|
||||
p = &presenter{
|
||||
colors: parent.colors,
|
||||
name: name,
|
||||
prefix: parent.prefix,
|
||||
}
|
||||
} else {
|
||||
cf := monochrome
|
||||
if l.color {
|
||||
if name == api.WatchLogger {
|
||||
cf = makeColorFunc("92")
|
||||
} else {
|
||||
cf = nextColor()
|
||||
}
|
||||
}
|
||||
p = &presenter{
|
||||
colors: cf,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
p := &presenter{
|
||||
colors: cf,
|
||||
name: name,
|
||||
}
|
||||
l.presenters.Store(name, p)
|
||||
l.computeWidth()
|
||||
if l.prefix {
|
||||
l.computeWidth()
|
||||
l.presenters.Range(func(key, value interface{}) bool {
|
||||
p := value.(*presenter)
|
||||
p.setPrefix(l.width)
|
||||
|
||||
@@ -106,7 +106,6 @@ type LogKeyboard struct {
|
||||
Watch KeyboardWatch
|
||||
IsDockerDesktopActive bool
|
||||
IsWatchConfigured bool
|
||||
IsDDComposeUIActive bool
|
||||
logLevel KEYBOARD_LOG_LEVEL
|
||||
signalChannel chan<- os.Signal
|
||||
}
|
||||
@@ -114,7 +113,7 @@ type LogKeyboard struct {
|
||||
var KeyboardManager *LogKeyboard
|
||||
var eg multierror.Group
|
||||
|
||||
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured, isDockerDesktopConfigActive bool,
|
||||
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
|
||||
sc chan<- os.Signal,
|
||||
watchFn func(ctx context.Context,
|
||||
doneCh chan bool,
|
||||
@@ -126,7 +125,6 @@ func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfi
|
||||
km := LogKeyboard{}
|
||||
km.IsDockerDesktopActive = isDockerDesktopActive
|
||||
km.IsWatchConfigured = isWatchConfigured
|
||||
km.IsDDComposeUIActive = isDockerDesktopConfigActive
|
||||
km.logLevel = INFO
|
||||
|
||||
km.Watch.Watching = false
|
||||
@@ -200,9 +198,10 @@ func (lk *LogKeyboard) navigationMenu() string {
|
||||
if openDDInfo != "" {
|
||||
openDDUI = navColor(" ")
|
||||
}
|
||||
if lk.IsDDComposeUIActive {
|
||||
if lk.IsDockerDesktopActive {
|
||||
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
|
||||
}
|
||||
|
||||
var watchInfo string
|
||||
if openDDInfo != "" || openDDUI != "" {
|
||||
watchInfo = navColor(" ")
|
||||
@@ -246,7 +245,7 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
|
||||
if !lk.IsDDComposeUIActive {
|
||||
if !lk.IsDockerDesktopActive {
|
||||
return
|
||||
}
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
|
||||
@@ -288,19 +287,7 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
||||
|
||||
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
|
||||
if !lk.IsWatchConfigured {
|
||||
if lk.IsDDComposeUIActive {
|
||||
// we try to open watch docs
|
||||
lk.openDDWatchDocs(ctx, project)
|
||||
}
|
||||
// either way we mark menu/watch as an error
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := fmt.Errorf("Watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
||||
lk.keyboardError("Watch", err)
|
||||
return err
|
||||
}))
|
||||
return
|
||||
|
||||
}
|
||||
lk.Watch.switchWatching()
|
||||
if !lk.Watch.isWatching() {
|
||||
@@ -330,6 +317,20 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
|
||||
case 'v':
|
||||
lk.openDockerDesktop(ctx, project)
|
||||
case 'w':
|
||||
if !lk.IsWatchConfigured {
|
||||
// we try to open watch docs if DD is installed
|
||||
if lk.IsDockerDesktopActive {
|
||||
lk.openDDWatchDocs(ctx, project)
|
||||
}
|
||||
// either way we mark menu/watch as an error
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
||||
lk.keyboardError("Watch", err)
|
||||
return err
|
||||
}))
|
||||
return
|
||||
}
|
||||
lk.StartWatch(ctx, doneCh, project, options)
|
||||
case 'o':
|
||||
lk.openDDComposeUI(ctx, project)
|
||||
|
||||
@@ -19,6 +19,7 @@ Define and run multi-container applications with Docker
|
||||
| [`down`](compose_down.md) | Stop and remove containers, networks |
|
||||
| [`events`](compose_events.md) | Receive real time events from containers |
|
||||
| [`exec`](compose_exec.md) | Execute a command in a running container |
|
||||
| [`export`](compose_export.md) | Export a service container's filesystem as a tar archive |
|
||||
| [`images`](compose_images.md) | List images used by the created containers |
|
||||
| [`kill`](compose_kill.md) | Force stop service containers |
|
||||
| [`logs`](compose_logs.md) | View output from containers |
|
||||
|
||||
17
docs/reference/compose_alpha_generate.md
Normal file
17
docs/reference/compose_alpha_generate.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# docker compose alpha generate
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
|
||||
| `--name` | `string` | | Project name to set in the Compose file |
|
||||
| `--project-dir` | `string` | | Directory to use for the project |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -7,6 +7,7 @@ Copy files/folders between a service container and the local filesystem
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------------|:-------|:--------|:--------------------------------------------------------|
|
||||
| `--all` | `bool` | | Include containers created by the run command |
|
||||
| `-a`, `--archive` | `bool` | | Archive mode (copy all uid/gid information) |
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `-L`, `--follow-link` | `bool` | | Always follow symbol link in SRC_PATH |
|
||||
|
||||
16
docs/reference/compose_export.md
Normal file
16
docs/reference/compose_export.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# docker compose export
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Export a service container's filesystem as a tar archive
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:-----------------|:---------|:--------|:---------------------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
|
||||
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -13,6 +13,7 @@ cname:
|
||||
- docker compose down
|
||||
- docker compose events
|
||||
- docker compose exec
|
||||
- docker compose export
|
||||
- docker compose images
|
||||
- docker compose kill
|
||||
- docker compose logs
|
||||
@@ -44,6 +45,7 @@ clink:
|
||||
- docker_compose_down.yaml
|
||||
- docker_compose_events.yaml
|
||||
- docker_compose_exec.yaml
|
||||
- docker_compose_export.yaml
|
||||
- docker_compose_images.yaml
|
||||
- docker_compose_kill.yaml
|
||||
- docker_compose_logs.yaml
|
||||
|
||||
@@ -4,9 +4,11 @@ long: Experimental commands
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
cname:
|
||||
- docker compose alpha generate
|
||||
- docker compose alpha publish
|
||||
- docker compose alpha viz
|
||||
clink:
|
||||
- docker_compose_alpha_generate.yaml
|
||||
- docker_compose_alpha_publish.yaml
|
||||
- docker_compose_alpha_viz.yaml
|
||||
inherited_options:
|
||||
|
||||
53
docs/reference/docker_compose_alpha_generate.yaml
Normal file
53
docs/reference/docker_compose_alpha_generate.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
command: docker compose alpha generate
|
||||
short: EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
long: EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
usage: docker compose alpha generate [OPTIONS] [CONTAINERS...]
|
||||
pname: docker compose alpha
|
||||
plink: docker_compose_alpha.yaml
|
||||
options:
|
||||
- option: format
|
||||
value_type: string
|
||||
default_value: yaml
|
||||
description: 'Format the output. Values: [yaml | json]'
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: name
|
||||
value_type: string
|
||||
description: Project name to set in the Compose file
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: project-dir
|
||||
value_type: string
|
||||
description: Directory to use for the project
|
||||
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
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: true
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
command: docker compose alpha publish
|
||||
short: Publish compose application
|
||||
long: Publish compose application
|
||||
usage: docker compose alpha publish [OPTIONS] [REPOSITORY]
|
||||
usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
|
||||
pname: docker compose alpha
|
||||
plink: docker_compose_alpha.yaml
|
||||
options:
|
||||
|
||||
@@ -10,9 +10,9 @@ options:
|
||||
- option: all
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Copy to all the containers of the service
|
||||
deprecated: true
|
||||
hidden: true
|
||||
description: Include containers created by the run command
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
|
||||
45
docs/reference/docker_compose_export.yaml
Normal file
45
docs/reference/docker_compose_export.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
command: docker compose export
|
||||
short: Export a service container's filesystem as a tar archive
|
||||
long: Export a service container's filesystem as a tar archive
|
||||
usage: docker compose export [OPTIONS] SERVICE
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
options:
|
||||
- option: index
|
||||
value_type: int
|
||||
default_value: "0"
|
||||
description: index of the container if service has multiple replicas.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: output
|
||||
shorthand: o
|
||||
value_type: string
|
||||
description: Write to a file, instead of STDOUT
|
||||
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
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
28
go.mod
28
go.mod
@@ -1,21 +1,21 @@
|
||||
module github.com/docker/compose/v2
|
||||
|
||||
go 1.21.0
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0
|
||||
github.com/containerd/containerd v1.7.22
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4
|
||||
github.com/containerd/containerd v1.7.23
|
||||
github.com/containerd/platforms v0.2.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/buildx v0.17.1
|
||||
github.com/docker/cli v27.3.0-rc.2+incompatible
|
||||
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible
|
||||
github.com/docker/cli-docs-tool v0.8.0
|
||||
github.com/docker/docker v27.3.0-rc.2+incompatible
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||
@@ -50,13 +50,14 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.21.0
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.4.0
|
||||
go.uber.org/mock v0.5.0
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.25.0
|
||||
google.golang.org/grpc v1.62.0
|
||||
golang.org/x/sys v0.26.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.1
|
||||
tags.cncf.io/container-device-interface v0.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -79,11 +80,11 @@ require (
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||
github.com/containerd/continuity v0.4.3 // indirect
|
||||
github.com/containerd/errdefs v0.1.0 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||
@@ -175,9 +176,9 @@ require (
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -190,5 +191,4 @@ require (
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
tags.cncf.io/container-device-interface v0.8.0 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -1,7 +1,7 @@
|
||||
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
|
||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
@@ -77,28 +77,28 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
|
||||
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
|
||||
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg=
|
||||
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4 h1:cvHBl5Jf1iNBmRrZCICmHvaoskYc1etTPEMLKVwokAY=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd v1.7.22 h1:nZuNnNRA6T6jB975rx2RRNqqH2k6ELYKDZfqTHqwyy0=
|
||||
github.com/containerd/containerd v1.7.22/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
|
||||
github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
|
||||
github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
|
||||
github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA=
|
||||
github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig=
|
||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
|
||||
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
|
||||
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
|
||||
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
|
||||
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
@@ -128,15 +128,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/buildx v0.17.1 h1:9ob2jGp4+W9PxWw68GsoNFp+eYFc7eUoRL9VljLCSM4=
|
||||
github.com/docker/buildx v0.17.1/go.mod h1:kJOhOhS47LRvrLFRulFiO5SE6VJf54yYMn7DzjgO5W0=
|
||||
github.com/docker/cli v27.3.0-rc.2+incompatible h1:z/lQXreGSzWl0RySxyFOm6WygLQtre2EkB4SCwsEut0=
|
||||
github.com/docker/cli v27.3.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible h1:fJ3SzYiebfWoas3qOJpmRsNrDL2w1XIpPFywSLFhhfk=
|
||||
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.8.0 h1:YcDWl7rQJC3lJ7WVZRwSs3bc9nka97QLWfyJQli8yJU=
|
||||
github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsBIrW21a5pUbdk=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.3.0-rc.2+incompatible h1:+WSuhLOwbip2IuVKZ4ecSQfG/grbPw96d3b7jBC8MSQ=
|
||||
github.com/docker/docker v27.3.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@@ -158,8 +158,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
@@ -199,8 +199,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
|
||||
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
|
||||
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -520,8 +520,8 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -582,8 +582,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -610,15 +610,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
|
||||
|
||||
@@ -27,11 +27,15 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"github.com/docker/compose/v2/internal/memnet"
|
||||
"github.com/r3labs/sse"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
// identify this client in the logs
|
||||
var userAgent = "compose/" + internal.Version
|
||||
|
||||
// Client for integration with Docker Desktop features.
|
||||
type Client struct {
|
||||
apiEndpoint string
|
||||
@@ -76,6 +80,7 @@ func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -105,6 +110,7 @@ func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlagResponse, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -135,6 +141,7 @@ func (c *Client) GetFileSharesConfig(ctx context.Context) (*GetFileSharesConfigR
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -170,6 +177,7 @@ func (c *Client) CreateFileShare(ctx context.Context, r CreateFileShareRequest)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -212,6 +220,7 @@ func (c *Client) ListFileShares(ctx context.Context) ([]FileShareSession, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -236,6 +245,7 @@ func (c *Client) DeleteFileShare(ctx context.Context, id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -268,6 +278,7 @@ func (c *Client) StreamFileShares(ctx context.Context) (<-chan EventMessage[[]Fi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -66,20 +66,3 @@ func (s *State) Load(ctx context.Context, client *desktop.Client) error {
|
||||
s.desktopValues = desktopValues
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) NavBar() bool {
|
||||
return s.determineFeatureState("ComposeNav")
|
||||
}
|
||||
|
||||
func (s *State) ComposeUI() bool {
|
||||
return s.determineFeatureState("ComposeUIView")
|
||||
}
|
||||
|
||||
func (s *State) determineFeatureState(name string) bool {
|
||||
if s == nil || !s.active || s.desktopValues == nil {
|
||||
return false
|
||||
}
|
||||
// TODO(milas): we should add individual environment variable overrides
|
||||
// per-experiment in a generic way here
|
||||
return s.desktopValues[name].Enabled
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha
|
||||
config = v1.DescriptorEmptyJSON
|
||||
artifactType = ComposeProjectArtifactType
|
||||
// N.B. the descriptor has the data embedded in it
|
||||
toPush = append(toPush, Pushable{Descriptor: config, Data: nil})
|
||||
toPush = append(toPush, Pushable{Descriptor: config, Data: make([]byte, len(config.Data))})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
@@ -63,11 +61,7 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr
|
||||
}
|
||||
}
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
conn, err := grpc.DialContext(
|
||||
dialCtx,
|
||||
cfg.Endpoint,
|
||||
conn, err := grpc.NewClient(cfg.Endpoint,
|
||||
grpc.WithContextDialer(memnet.DialEndpoint),
|
||||
// this dial is restricted to using a local Unix socket / named pipe,
|
||||
// so there is no need for TLS
|
||||
|
||||
@@ -22,19 +22,16 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI bool) {
|
||||
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured bool) {
|
||||
commandAvailable := []string{}
|
||||
if isDockerDesktopActive {
|
||||
commandAvailable = append(commandAvailable, "gui")
|
||||
commandAvailable = append(commandAvailable, "gui/composeview")
|
||||
}
|
||||
if isWatchConfigured {
|
||||
commandAvailable = append(commandAvailable, "watch")
|
||||
}
|
||||
|
||||
if isDockerDesktopComposeUI {
|
||||
commandAvailable = append(commandAvailable, "gui/composeview")
|
||||
}
|
||||
|
||||
AddAttributeToSpan(ctx,
|
||||
attribute.Bool("navmenu.enabled", enabled),
|
||||
attribute.StringSlice("navmenu.command_available", commandAvailable))
|
||||
|
||||
@@ -90,6 +90,10 @@ type Service interface {
|
||||
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
|
||||
// Scale manages numbers of container instances running per service
|
||||
Scale(ctx context.Context, project *types.Project, options ScaleOptions) error
|
||||
// Export a service container's filesystem as a tar archive
|
||||
Export(ctx context.Context, projectName string, options ExportOptions) error
|
||||
// Generate generates a Compose Project from existing containers
|
||||
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
|
||||
}
|
||||
|
||||
type ScaleOptions struct {
|
||||
@@ -289,6 +293,7 @@ type ConfigOptions struct {
|
||||
type PushOptions struct {
|
||||
Quiet bool
|
||||
IgnoreFailures bool
|
||||
ImageMandatory bool
|
||||
}
|
||||
|
||||
// PullOptions group options of the Pull API
|
||||
@@ -553,6 +558,20 @@ type PauseOptions struct {
|
||||
Project *types.Project
|
||||
}
|
||||
|
||||
// ExportOptions group options of the Export API
|
||||
type ExportOptions struct {
|
||||
Service string
|
||||
Index int
|
||||
Output string
|
||||
}
|
||||
|
||||
type GenerateOptions struct {
|
||||
// ProjectName to set in the Compose file
|
||||
ProjectName string
|
||||
// Containers passed in the command line to be used as reference for service definition
|
||||
Containers []string
|
||||
}
|
||||
|
||||
const (
|
||||
// STARTING indicates that stack is being deployed
|
||||
STARTING string = "Starting"
|
||||
@@ -628,6 +647,8 @@ const (
|
||||
ContainerEventExit
|
||||
// UserCancel user cancelled compose up, we are stopping containers
|
||||
UserCancel
|
||||
// HookEventLog is a ContainerEvent of type log on stdout by service hook
|
||||
HookEventLog
|
||||
)
|
||||
|
||||
// Separator is used for naming components
|
||||
|
||||
@@ -176,7 +176,10 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
|
||||
}
|
||||
set := types.Services{}
|
||||
for _, c := range containers {
|
||||
serviceLabel := c.Labels[api.ServiceLabel]
|
||||
serviceLabel, ok := c.Labels[api.ServiceLabel]
|
||||
if !ok {
|
||||
serviceLabel = getCanonicalContainerName(c)
|
||||
}
|
||||
service, ok := set[serviceLabel]
|
||||
if !ok {
|
||||
service = types.ServiceConfig{
|
||||
@@ -321,30 +324,6 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
|
||||
|
||||
}
|
||||
|
||||
var windowsContainer = struct {
|
||||
once sync.Once
|
||||
val bool
|
||||
err error
|
||||
}{}
|
||||
|
||||
func (s *composeService) isWindowsContainer(ctx context.Context) (bool, error) {
|
||||
windowsContainer.once.Do(func() {
|
||||
info, err := s.apiClient().Info(ctx)
|
||||
if err != nil {
|
||||
windowsContainer.err = err
|
||||
}
|
||||
windowsContainer.val = info.OSType == "windows"
|
||||
})
|
||||
return windowsContainer.val, windowsContainer.err
|
||||
}
|
||||
|
||||
func (s *composeService) isDesktopIntegrationActive() bool {
|
||||
return s.desktopCli != nil
|
||||
}
|
||||
|
||||
func (s *composeService) isDesktopUIEnabled() bool {
|
||||
if !s.isDesktopIntegrationActive() {
|
||||
return false
|
||||
}
|
||||
return s.experiments.ComposeUI()
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
container := container
|
||||
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(container)...)
|
||||
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "service/scale/down", traceOpts, func(ctx context.Context) error {
|
||||
return c.service.stopAndRemoveContainer(ctx, container, timeout, false)
|
||||
return c.service.stopAndRemoveContainer(ctx, container, &service, timeout, false)
|
||||
}))
|
||||
continue
|
||||
}
|
||||
@@ -223,8 +223,8 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
|
||||
// Stop dependent containers, so they will be restarted after service is re-created
|
||||
dependents := project.GetDependentsForService(service)
|
||||
for _, name := range dependents {
|
||||
dependents := c.observedState[name]
|
||||
err := c.service.stopContainers(ctx, w, dependents, nil)
|
||||
dependents := c.getObservedState(name)
|
||||
err := c.service.stopContainers(ctx, w, &service, dependents, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
|
||||
dependent.State = ContainerExited
|
||||
dependents[i] = dependent
|
||||
}
|
||||
c.observedState[name] = dependents
|
||||
c.setObservedState(name, dependents)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -364,7 +364,12 @@ func containerReasonEvents(containers Containers, eventFunc func(string, string)
|
||||
const ServiceConditionRunningOrHealthy = "running_or_healthy"
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependant string, dependencies types.DependsOnConfig, containers Containers) error {
|
||||
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependant string, dependencies types.DependsOnConfig, containers Containers, timeout time.Duration) error {
|
||||
if timeout > 0 {
|
||||
withTimeout, cancelFunc := context.WithTimeout(ctx, timeout)
|
||||
defer cancelFunc()
|
||||
ctx = withTimeout
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
w := progress.ContextWriter(ctx)
|
||||
for dep, config := range dependencies {
|
||||
@@ -454,7 +459,11 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
}
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
err := eg.Wait()
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return fmt.Errorf("timeout waiting for dependencies")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceDependency, project *types.Project) (bool, error) {
|
||||
@@ -760,12 +769,15 @@ func (s *composeService) isServiceCompleted(ctx context.Context, containers Cont
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig, containers Containers) error {
|
||||
func (s *composeService) startService(ctx context.Context,
|
||||
project *types.Project, service types.ServiceConfig,
|
||||
containers Containers, listener api.ContainerEventListener,
|
||||
timeout time.Duration) error {
|
||||
if service.Deploy != nil && service.Deploy.Replicas != nil && *service.Deploy.Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, containers)
|
||||
err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, containers, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -784,10 +796,18 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
|
||||
}
|
||||
eventName := getContainerProgressName(container)
|
||||
w.Event(progress.StartingEvent(eventName))
|
||||
err := s.apiClient().ContainerStart(ctx, container.ID, containerType.StartOptions{})
|
||||
err = s.apiClient().ContainerStart(ctx, container.ID, containerType.StartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, hook := range service.PostStart {
|
||||
err = s.runHook(ctx, container, service, hook, listener)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.Event(progress.StartedEvent(eventName))
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
@@ -238,7 +239,7 @@ func TestWaitDependencies(t *testing.T) {
|
||||
"db": {Condition: ServiceConditionRunningOrHealthy},
|
||||
"redis": {Condition: ServiceConditionRunningOrHealthy},
|
||||
}
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil))
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
|
||||
})
|
||||
t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
|
||||
dbService := types.ServiceConfig{Name: "db", Scale: intPtr(1)}
|
||||
@@ -251,7 +252,7 @@ func TestWaitDependencies(t *testing.T) {
|
||||
"db": {Condition: types.ServiceConditionStarted, Required: true},
|
||||
"redis": {Condition: types.ServiceConditionStarted, Required: true},
|
||||
}
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil))
|
||||
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,10 +301,17 @@ func TestCreateMobyContainer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
|
||||
v := x.(*containerType.HostConfig)
|
||||
return v.NetworkMode == "b-moby-name"
|
||||
}), gomock.Eq(
|
||||
var falseBool bool
|
||||
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
|
||||
&containerType.HostConfig{
|
||||
PortBindings: nat.PortMap{},
|
||||
ExtraHosts: []string{},
|
||||
Tmpfs: map[string]string{},
|
||||
Resources: containerType.Resources{
|
||||
OomKillDisable: &falseBool,
|
||||
},
|
||||
NetworkMode: "b-moby-name",
|
||||
}), gomock.Eq(
|
||||
&network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
"b-moby-name": {
|
||||
@@ -382,10 +390,17 @@ func TestCreateMobyContainer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
|
||||
v := x.(*containerType.HostConfig)
|
||||
return v.NetworkMode == "b-moby-name"
|
||||
}), gomock.Eq(
|
||||
var falseBool bool
|
||||
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
|
||||
&containerType.HostConfig{
|
||||
PortBindings: nat.PortMap{},
|
||||
ExtraHosts: []string{},
|
||||
Tmpfs: map[string]string{},
|
||||
Resources: containerType.Resources{
|
||||
OomKillDisable: &falseBool,
|
||||
},
|
||||
NetworkMode: "b-moby-name",
|
||||
}), gomock.Eq(
|
||||
&network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
"a-moby-name": {
|
||||
|
||||
@@ -61,11 +61,6 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
|
||||
direction |= fromService
|
||||
serviceName = srcService
|
||||
copyFunc = s.copyFromContainer
|
||||
|
||||
// copying from multiple containers of a services doesn't make sense.
|
||||
if options.All {
|
||||
return errors.New("cannot use the --all flag when copying from a service")
|
||||
}
|
||||
}
|
||||
if destService != "" {
|
||||
direction |= toService
|
||||
@@ -80,7 +75,7 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
|
||||
return errors.New("unknown copy direction")
|
||||
}
|
||||
|
||||
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options.Index, direction, serviceName)
|
||||
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options, direction, serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,18 +114,22 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, index int, direction copyDirection, serviceName string) (Containers, error) {
|
||||
func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, options api.CopyOptions, direction copyDirection, serviceName string) (Containers, error) {
|
||||
var containers Containers
|
||||
var err error
|
||||
switch {
|
||||
case index > 0:
|
||||
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, index)
|
||||
case options.Index > 0:
|
||||
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, options.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(containers, ctr), nil
|
||||
default:
|
||||
containers, err = s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
|
||||
withOneOff := oneOffExclude
|
||||
if options.All {
|
||||
withOneOff = oneOffInclude
|
||||
}
|
||||
containers, err = s.getContainers(ctx, projectName, withOneOff, true, serviceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -48,6 +47,7 @@ import (
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/sirupsen/logrus"
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
@@ -104,7 +104,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
||||
orphans := observedState.filter(isOrphaned(project))
|
||||
if len(orphans) > 0 && !options.IgnoreOrphans {
|
||||
if options.RemoveOrphans {
|
||||
err := s.removeContainers(ctx, orphans, nil, false)
|
||||
err := s.removeContainers(ctx, orphans, nil, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -505,7 +505,10 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
|
||||
}
|
||||
labels[api.ConfigHashLabel] = hash
|
||||
|
||||
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
|
||||
if number > 0 {
|
||||
// One-off containers are not indexed
|
||||
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
|
||||
}
|
||||
|
||||
var dependencies []string
|
||||
for s, d := range service.DependsOn {
|
||||
@@ -645,7 +648,14 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
|
||||
setReservations(s.Deploy.Resources.Reservations, &resources)
|
||||
}
|
||||
|
||||
var cdiDeviceNames []string
|
||||
for _, device := range s.Devices {
|
||||
|
||||
if device.Source == device.Target && cdi.IsQualifiedName(device.Source) {
|
||||
cdiDeviceNames = append(cdiDeviceNames, device.Source)
|
||||
continue
|
||||
}
|
||||
|
||||
resources.Devices = append(resources.Devices, container.DeviceMapping{
|
||||
PathOnHost: device.Source,
|
||||
PathInContainer: device.Target,
|
||||
@@ -653,6 +663,23 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
|
||||
})
|
||||
}
|
||||
|
||||
if len(cdiDeviceNames) > 0 {
|
||||
resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
|
||||
Driver: "cdi",
|
||||
DeviceIDs: cdiDeviceNames,
|
||||
})
|
||||
}
|
||||
|
||||
for _, gpus := range s.Gpus {
|
||||
resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
|
||||
Driver: gpus.Driver,
|
||||
Count: int(gpus.Count),
|
||||
DeviceIDs: gpus.IDs,
|
||||
Capabilities: [][]string{append(gpus.Capabilities, "gpu")},
|
||||
Options: gpus.Options,
|
||||
})
|
||||
}
|
||||
|
||||
ulimits := toUlimits(s.Ulimits)
|
||||
resources.Ulimits = ulimits
|
||||
return resources
|
||||
@@ -694,6 +721,7 @@ func setReservations(reservations *types.Resource, resources *container.Resource
|
||||
Count: int(device.Count),
|
||||
DeviceIDs: device.IDs,
|
||||
Driver: device.Driver,
|
||||
Options: device.Options,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -801,28 +829,28 @@ func (s *composeService) buildContainerVolumes(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mountOptions, err := s.buildContainerMountOptions(ctx, p, service, imgInspect, inherit)
|
||||
mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
version, err := s.RuntimeVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if versions.GreaterThan(version, "1.42") {
|
||||
// We can fully leverage `Mount` API as a replacement for legacy `Bind`
|
||||
return nil, mountOptions, nil
|
||||
}
|
||||
|
||||
MOUNTS:
|
||||
for _, m := range mountOptions {
|
||||
if m.Type == mount.TypeNamedPipe {
|
||||
mounts = append(mounts, m)
|
||||
continue
|
||||
}
|
||||
if m.Type == mount.TypeBind {
|
||||
// `Mount` does not offer option to created host path if missing
|
||||
// `Mount` is preferred but does not offer option to created host path if missing
|
||||
// so `Bind` API is used here with raw volume string
|
||||
// see https://github.com/moby/moby/issues/43483
|
||||
for _, v := range service.Volumes {
|
||||
if v.Target == m.Target {
|
||||
if v.Bind != nil && v.Bind.CreateHostPath {
|
||||
switch {
|
||||
case string(m.Type) != v.Type:
|
||||
v.Source = m.Source
|
||||
fallthrough
|
||||
case !requireMountAPI(v.Bind):
|
||||
binds = append(binds, v.String())
|
||||
continue MOUNTS
|
||||
}
|
||||
@@ -834,7 +862,24 @@ MOUNTS:
|
||||
return binds, mounts, nil
|
||||
}
|
||||
|
||||
func (s *composeService) buildContainerMountOptions(ctx context.Context, p types.Project, service types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
|
||||
// requireMountAPI check if Bind declaration can be implemented by the plain old Bind API or uses any of the advanced
|
||||
// options which require use of Mount API
|
||||
func requireMountAPI(bind *types.ServiceVolumeBind) bool {
|
||||
switch {
|
||||
case bind == nil:
|
||||
return false
|
||||
case !bind.CreateHostPath:
|
||||
return true
|
||||
case bind.Propagation != "":
|
||||
return true
|
||||
case bind.Recursive != "":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
|
||||
var mounts = map[string]mount.Mount{}
|
||||
if inherit != nil {
|
||||
for _, m := range inherit.Mounts {
|
||||
@@ -845,7 +890,6 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
|
||||
if m.Type == "volume" {
|
||||
src = m.Name
|
||||
}
|
||||
m.Destination = path.Clean(m.Destination)
|
||||
|
||||
if img.Config != nil {
|
||||
if _, ok := img.Config.Volumes[m.Destination]; ok {
|
||||
@@ -859,7 +903,7 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
|
||||
}
|
||||
}
|
||||
volumes := []types.ServiceVolumeConfig{}
|
||||
for _, v := range service.Volumes {
|
||||
for _, v := range s.Volumes {
|
||||
if v.Target != m.Destination || v.Source != "" {
|
||||
volumes = append(volumes, v)
|
||||
continue
|
||||
@@ -872,11 +916,11 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
|
||||
ReadOnly: !m.RW,
|
||||
}
|
||||
}
|
||||
service.Volumes = volumes
|
||||
s.Volumes = volumes
|
||||
}
|
||||
}
|
||||
|
||||
mounts, err := s.fillBindMounts(ctx, p, service, mounts)
|
||||
mounts, err := fillBindMounts(p, s, mounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -888,27 +932,27 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, service types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
|
||||
for _, v := range service.Volumes {
|
||||
bindMount, err := s.buildMount(ctx, p, v)
|
||||
func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
|
||||
for _, v := range s.Volumes {
|
||||
bindMount, err := buildMount(p, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[bindMount.Target] = bindMount
|
||||
}
|
||||
|
||||
secrets, err := s.buildContainerSecretMounts(ctx, p, service)
|
||||
secrets, err := buildContainerSecretMounts(p, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if _, found := m[secret.Target]; found {
|
||||
for _, s := range secrets {
|
||||
if _, found := m[s.Target]; found {
|
||||
continue
|
||||
}
|
||||
m[secret.Target] = secret
|
||||
m[s.Target] = s
|
||||
}
|
||||
|
||||
configs, err := s.buildContainerConfigMounts(ctx, p, service)
|
||||
configs, err := buildContainerConfigMounts(p, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -921,11 +965,11 @@ func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, se
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
|
||||
func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
|
||||
var mounts = map[string]mount.Mount{}
|
||||
|
||||
configsBaseDir := "/"
|
||||
for _, config := range service.Configs {
|
||||
for _, config := range s.Configs {
|
||||
target := config.Target
|
||||
if config.Target == "" {
|
||||
target = configsBaseDir + config.Source
|
||||
@@ -933,10 +977,6 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
|
||||
target = configsBaseDir + config.Target
|
||||
}
|
||||
|
||||
if config.UID != "" || config.GID != "" || config.Mode != nil {
|
||||
logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored")
|
||||
}
|
||||
|
||||
definedConfig := p.Configs[config.Source]
|
||||
if definedConfig.External {
|
||||
return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name)
|
||||
@@ -953,7 +993,11 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
|
||||
continue
|
||||
}
|
||||
|
||||
bindMount, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
|
||||
if config.UID != "" || config.GID != "" || config.Mode != nil {
|
||||
logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored")
|
||||
}
|
||||
|
||||
bindMount, err := buildMount(p, types.ServiceVolumeConfig{
|
||||
Type: types.VolumeTypeBind,
|
||||
Source: definedConfig.File,
|
||||
Target: target,
|
||||
@@ -971,11 +1015,11 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
|
||||
func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
|
||||
var mounts = map[string]mount.Mount{}
|
||||
|
||||
secretsDir := "/run/secrets/"
|
||||
for _, secret := range service.Secrets {
|
||||
for _, secret := range s.Secrets {
|
||||
target := secret.Target
|
||||
if secret.Target == "" {
|
||||
target = secretsDir + secret.Source
|
||||
@@ -983,10 +1027,6 @@ func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types
|
||||
target = secretsDir + secret.Target
|
||||
}
|
||||
|
||||
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
|
||||
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored")
|
||||
}
|
||||
|
||||
definedSecret := p.Secrets[secret.Source]
|
||||
if definedSecret.External {
|
||||
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
|
||||
@@ -1003,11 +1043,22 @@ func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types
|
||||
continue
|
||||
}
|
||||
|
||||
mnt, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
|
||||
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
|
||||
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(definedSecret.File); os.IsNotExist(err) {
|
||||
logrus.Warnf("secret file %s does not exist", definedSecret.Name)
|
||||
}
|
||||
|
||||
mnt, err := buildMount(p, types.ServiceVolumeConfig{
|
||||
Type: types.VolumeTypeBind,
|
||||
Source: definedSecret.File,
|
||||
Target: target,
|
||||
ReadOnly: true,
|
||||
Bind: &types.ServiceVolumeBind{
|
||||
CreateHostPath: false,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1039,7 +1090,7 @@ func isWindowsAbs(p string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *composeService) buildMount(ctx context.Context, project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
source := volume.Source
|
||||
// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.
|
||||
// do not replace these with filepath.Abs(source) that will include a default drive.
|
||||
@@ -1060,12 +1111,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
|
||||
}
|
||||
}
|
||||
|
||||
bind, vol, tmpfs, err := s.buildMountOptions(ctx, volume)
|
||||
if err != nil {
|
||||
return mount.Mount{}, err
|
||||
}
|
||||
|
||||
volume.Target = path.Clean(volume.Target)
|
||||
bind, vol, tmpfs := buildMountOptions(volume)
|
||||
|
||||
if bind != nil {
|
||||
volume.Type = types.VolumeTypeBind
|
||||
@@ -1083,7 +1129,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *composeService) buildMountOptions(ctx context.Context, volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, error) {
|
||||
func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) {
|
||||
switch volume.Type {
|
||||
case "bind":
|
||||
if volume.Volume != nil {
|
||||
@@ -1092,8 +1138,7 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
|
||||
if volume.Tmpfs != nil {
|
||||
logrus.Warnf("mount of type `bind` should not define `tmpfs` option")
|
||||
}
|
||||
option, err := s.buildBindOption(ctx, volume.Bind)
|
||||
return option, nil, nil, err
|
||||
return buildBindOption(volume.Bind), nil, nil
|
||||
case "volume":
|
||||
if volume.Bind != nil {
|
||||
logrus.Warnf("mount of type `volume` should not define `bind` option")
|
||||
@@ -1101,7 +1146,7 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
|
||||
if volume.Tmpfs != nil {
|
||||
logrus.Warnf("mount of type `volume` should not define `tmpfs` option")
|
||||
}
|
||||
return nil, buildVolumeOptions(volume.Volume), nil, nil
|
||||
return nil, buildVolumeOptions(volume.Volume), nil
|
||||
case "tmpfs":
|
||||
if volume.Bind != nil {
|
||||
logrus.Warnf("mount of type `tmpfs` should not define `bind` option")
|
||||
@@ -1109,30 +1154,28 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
|
||||
if volume.Volume != nil {
|
||||
logrus.Warnf("mount of type `tmpfs` should not define `volume` option")
|
||||
}
|
||||
return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil
|
||||
return nil, nil, buildTmpfsOptions(volume.Tmpfs)
|
||||
}
|
||||
return nil, nil, nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *composeService) buildBindOption(ctx context.Context, bind *types.ServiceVolumeBind) (*mount.BindOptions, error) {
|
||||
func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
|
||||
if bind == nil {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
propagation := bind.Propagation
|
||||
isWindowsContainer, err := s.isWindowsContainer(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if propagation == "" && !isWindowsContainer {
|
||||
propagation = types.PropagationRPrivate
|
||||
}
|
||||
|
||||
return &mount.BindOptions{
|
||||
Propagation: mount.Propagation(propagation),
|
||||
opts := &mount.BindOptions{
|
||||
Propagation: mount.Propagation(bind.Propagation),
|
||||
CreateMountpoint: bind.CreateHostPath,
|
||||
// NonRecursive: false, FIXME missing from model ?
|
||||
}, nil
|
||||
}
|
||||
switch bind.Recursive {
|
||||
case "disabled":
|
||||
opts.NonRecursive = true
|
||||
case "writable":
|
||||
opts.ReadOnlyNonRecursive = true
|
||||
case "readonly":
|
||||
opts.ReadOnlyForceRecursive = true
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
|
||||
@@ -1190,7 +1233,13 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, n *types.Ne
|
||||
"Set `external: true` to use an existing network", n.Name, expectedProjectLabel)
|
||||
}
|
||||
if inspect.Labels[api.NetworkLabel] != expectedNetworkLabel {
|
||||
return fmt.Errorf("network %s was found but has incorrect label %s set to %q", n.Name, api.NetworkLabel, inspect.Labels[api.NetworkLabel])
|
||||
return fmt.Errorf(
|
||||
"network %s was found but has incorrect label %s set to %q (expected: %q)",
|
||||
n.Name,
|
||||
api.NetworkLabel,
|
||||
inspect.Labels[api.NetworkLabel],
|
||||
expectedNetworkLabel,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1314,7 +1363,6 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
|
||||
|
||||
switch len(networks) {
|
||||
case 1:
|
||||
n.Name = networks[0].ID
|
||||
return nil
|
||||
case 0:
|
||||
enabled, err := s.isSWarmEnabled(ctx)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -42,8 +41,7 @@ func TestBuildBindMount(t *testing.T) {
|
||||
Source: "",
|
||||
Target: "/data",
|
||||
}
|
||||
s := composeService{}
|
||||
mount, err := s.buildMount(context.TODO(), project, volume)
|
||||
mount, err := buildMount(project, volume)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, filepath.IsAbs(mount.Source))
|
||||
_, err = os.Stat(mount.Source)
|
||||
@@ -58,8 +56,7 @@ func TestBuildNamedPipeMount(t *testing.T) {
|
||||
Source: "\\\\.\\pipe\\docker_engine_windows",
|
||||
Target: "\\\\.\\pipe\\docker_engine",
|
||||
}
|
||||
s := composeService{}
|
||||
mount, err := s.buildMount(context.TODO(), project, volume)
|
||||
mount, err := buildMount(project, volume)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, mount.Type, mountTypes.TypeNamedPipe)
|
||||
}
|
||||
@@ -78,8 +75,7 @@ func TestBuildVolumeMount(t *testing.T) {
|
||||
Source: "myVolume",
|
||||
Target: "/data",
|
||||
}
|
||||
s := composeService{}
|
||||
mount, err := s.buildMount(context.TODO(), project, volume)
|
||||
mount, err := buildMount(project, volume)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, mount.Source, "myProject_myVolume")
|
||||
assert.Equal(t, mount.Type, mountTypes.TypeVolume)
|
||||
@@ -156,8 +152,8 @@ func TestBuildContainerMountOptions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
s := composeService{}
|
||||
mounts, err := s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
|
||||
|
||||
mounts, err := buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
@@ -169,7 +165,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
|
||||
assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
|
||||
assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
|
||||
|
||||
mounts, err = s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
|
||||
mounts, err = buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
|
||||
@@ -67,18 +67,26 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
|
||||
// Check requested services exists in model
|
||||
options.Services, err = checkSelectedServices(options, project)
|
||||
services, err := checkSelectedServices(options, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(options.Services) > 0 && len(services) == 0 {
|
||||
logrus.Infof("Any of the services %v not running in project %q", options.Services, projectName)
|
||||
return nil
|
||||
}
|
||||
|
||||
options.Services = services
|
||||
|
||||
if len(containers) > 0 {
|
||||
resourceToRemove = true
|
||||
}
|
||||
|
||||
err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
|
||||
serviceContainers := containers.filter(isService(service))
|
||||
err := s.removeContainers(ctx, serviceContainers, options.Timeout, options.Volumes)
|
||||
serv := project.Services[service]
|
||||
err := s.removeContainers(ctx, serviceContainers, &serv, options.Timeout, options.Volumes)
|
||||
return err
|
||||
}, WithRootNodesAndDown(options.Services))
|
||||
if err != nil {
|
||||
@@ -87,7 +95,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
|
||||
orphans := containers.filter(isOrphaned(project))
|
||||
if options.RemoveOrphans && len(orphans) > 0 {
|
||||
err := s.removeContainers(ctx, orphans, options.Timeout, false)
|
||||
err := s.removeContainers(ctx, orphans, nil, options.Timeout, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -289,9 +297,19 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
|
||||
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, service *types.ServiceConfig, container moby.Container, timeout *time.Duration) error {
|
||||
eventName := getContainerProgressName(container)
|
||||
w.Event(progress.StoppingEvent(eventName))
|
||||
|
||||
if service != nil {
|
||||
for _, hook := range service.PreStop {
|
||||
err := s.runHook(ctx, container, *service, hook, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeoutInSecond := utils.DurationSecondToInt(timeout)
|
||||
err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
|
||||
if err != nil {
|
||||
@@ -302,32 +320,32 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
|
||||
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, serv *types.ServiceConfig, containers []moby.Container, timeout *time.Duration) error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, container := range containers {
|
||||
container := container
|
||||
eg.Go(func() error {
|
||||
return s.stopContainer(ctx, w, container, timeout)
|
||||
return s.stopContainer(ctx, w, serv, container, timeout)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, timeout *time.Duration, volumes bool) error {
|
||||
func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
for _, container := range containers {
|
||||
container := container
|
||||
eg.Go(func() error {
|
||||
return s.stopAndRemoveContainer(ctx, container, timeout, volumes)
|
||||
return s.stopAndRemoveContainer(ctx, container, service, timeout, volumes)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, timeout *time.Duration, volumes bool) error {
|
||||
func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
||||
w := progress.ContextWriter(ctx)
|
||||
eventName := getContainerProgressName(container)
|
||||
err := s.stopContainer(ctx, w, container, timeout)
|
||||
err := s.stopContainer(ctx, w, service, container, timeout)
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.RemovedEvent(eventName))
|
||||
return nil
|
||||
|
||||
@@ -96,6 +96,95 @@ func TestDown(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestDownWithGivenServices(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api, cli := prepareMocks(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]moby.Container{
|
||||
testContainer("service1", "123", false),
|
||||
testContainer("service2", "456", false),
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
|
||||
// network names are not guaranteed to be unique, ensure Compose handles
|
||||
// cleanup properly if duplicates are inadvertently created
|
||||
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]network.Summary{
|
||||
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
}, nil)
|
||||
|
||||
stopOptions := containerType.StopOptions{}
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
|
||||
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "123", containerType.RemoveOptions{Force: true}).Return(nil)
|
||||
|
||||
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(strings.ToLower(testProject)),
|
||||
networkFilter("default")),
|
||||
}).Return([]network.Summary{
|
||||
{ID: "abc123", Name: "myProject_default"},
|
||||
}, nil)
|
||||
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
|
||||
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
|
||||
Services: []string{"service1", "not-running-service"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestDownWithSpecifiedServiceButTheServicesAreNotRunning(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
api, cli := prepareMocks(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: cli,
|
||||
}
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]moby.Container{
|
||||
testContainer("service1", "123", false),
|
||||
testContainer("service2", "456", false),
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
|
||||
// network names are not guaranteed to be unique, ensure Compose handles
|
||||
// cleanup properly if duplicates are inadvertently created
|
||||
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]network.Summary{
|
||||
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
}, nil)
|
||||
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
|
||||
Services: []string{"not-running-service1", "not-running-service2"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestDownRemoveOrphans(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
101
pkg/compose/export.go
Normal file
101
pkg/compose/export.go
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
return s.export(ctx, projectName, options)
|
||||
}, s.stdinfo(), "Exporting")
|
||||
}
|
||||
|
||||
func (s *composeService) export(ctx context.Context, projectName string, options api.ExportOptions) error {
|
||||
projectName = strings.ToLower(projectName)
|
||||
|
||||
container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Output == "" && s.dockerCli.Out().IsTerminal() {
|
||||
return fmt.Errorf("output option is required when exporting to terminal")
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(options.Output); err != nil {
|
||||
return fmt.Errorf("failed to export container: %w", err)
|
||||
}
|
||||
|
||||
clnt := s.dockerCli.Client()
|
||||
|
||||
w := progress.ContextWriter(ctx)
|
||||
|
||||
name := getCanonicalContainerName(container)
|
||||
msg := fmt.Sprintf("export %s to %s", name, options.Output)
|
||||
|
||||
w.Event(progress.Event{
|
||||
ID: name,
|
||||
Text: msg,
|
||||
Status: progress.Working,
|
||||
StatusText: "Exporting",
|
||||
})
|
||||
|
||||
responseBody, err := clnt.ContainerExport(ctx, container.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := responseBody.Close(); err != nil {
|
||||
w.Event(progress.Event{
|
||||
ID: name,
|
||||
Text: msg,
|
||||
Status: progress.Error,
|
||||
StatusText: fmt.Sprintf("Failed to close response body: %v", err),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
if !s.dryRun {
|
||||
if options.Output == "" {
|
||||
_, err := io.Copy(s.dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := command.CopyToFile(options.Output, responseBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.Event(progress.Event{
|
||||
ID: name,
|
||||
Text: msg,
|
||||
Status: progress.Done,
|
||||
StatusText: "Exported",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
247
pkg/compose/generate.go
Normal file
247
pkg/compose/generate.go
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
|
||||
filtersListNames := filters.NewArgs()
|
||||
filtersListIDs := filters.NewArgs()
|
||||
for _, containerName := range options.Containers {
|
||||
filtersListNames.Add("name", containerName)
|
||||
filtersListIDs.Add("id", containerName)
|
||||
}
|
||||
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
|
||||
Filters: filtersListNames,
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containersByIds, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
|
||||
Filters: filtersListIDs,
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, container := range containersByIds {
|
||||
if !utils.Contains(containers, container) {
|
||||
containers = append(containers, container)
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
return nil, fmt.Errorf("no container(s) found with the following name(s): %s", strings.Join(options.Containers, ","))
|
||||
}
|
||||
|
||||
return s.createProjectFromContainers(containers, options.ProjectName)
|
||||
}
|
||||
|
||||
func (s *composeService) createProjectFromContainers(containers []moby.Container, projectName string) (*types.Project, error) {
|
||||
project := &types.Project{}
|
||||
services := types.Services{}
|
||||
networks := types.Networks{}
|
||||
volumes := types.Volumes{}
|
||||
secrets := types.Secrets{}
|
||||
|
||||
if projectName != "" {
|
||||
project.Name = projectName
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
// if the container is from a previous Compose application, use the existing service name
|
||||
serviceLabel, ok := c.Labels[api.ServiceLabel]
|
||||
if !ok {
|
||||
serviceLabel = getCanonicalContainerName(c)
|
||||
}
|
||||
service, ok := services[serviceLabel]
|
||||
if !ok {
|
||||
service = types.ServiceConfig{
|
||||
Name: serviceLabel,
|
||||
Image: c.Image,
|
||||
Labels: c.Labels,
|
||||
}
|
||||
|
||||
}
|
||||
service.Scale = increment(service.Scale)
|
||||
|
||||
inspect, err := s.apiClient().ContainerInspect(context.Background(), c.ID)
|
||||
if err != nil {
|
||||
services[serviceLabel] = service
|
||||
continue
|
||||
}
|
||||
s.extractComposeConfiguration(&service, inspect, volumes, secrets, networks)
|
||||
service.Labels = cleanDockerPreviousLabels(service.Labels)
|
||||
services[serviceLabel] = service
|
||||
}
|
||||
|
||||
project.Services = services
|
||||
project.Networks = networks
|
||||
project.Volumes = volumes
|
||||
project.Secrets = secrets
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *composeService) extractComposeConfiguration(service *types.ServiceConfig, inspect moby.ContainerJSON, volumes types.Volumes, secrets types.Secrets, networks types.Networks) {
|
||||
service.Environment = types.NewMappingWithEquals(inspect.Config.Env)
|
||||
if inspect.Config.Healthcheck != nil {
|
||||
healthConfig := inspect.Config.Healthcheck
|
||||
service.HealthCheck = s.toComposeHealthCheck(healthConfig)
|
||||
}
|
||||
if len(inspect.Mounts) > 0 {
|
||||
detectedVolumes, volumeConfigs, detectedSecrets, secretsConfigs := s.toComposeVolumes(inspect.Mounts)
|
||||
service.Volumes = append(service.Volumes, volumeConfigs...)
|
||||
service.Secrets = append(service.Secrets, secretsConfigs...)
|
||||
maps.Copy(volumes, detectedVolumes)
|
||||
maps.Copy(secrets, detectedSecrets)
|
||||
}
|
||||
if len(inspect.NetworkSettings.Networks) > 0 {
|
||||
detectedNetworks, networkConfigs := s.toComposeNetwork(inspect.NetworkSettings.Networks)
|
||||
service.Networks = networkConfigs
|
||||
maps.Copy(networks, detectedNetworks)
|
||||
}
|
||||
if len(inspect.HostConfig.PortBindings) > 0 {
|
||||
for key, portBindings := range inspect.HostConfig.PortBindings {
|
||||
for _, portBinding := range portBindings {
|
||||
service.Ports = append(service.Ports, types.ServicePortConfig{
|
||||
Target: uint32(key.Int()),
|
||||
Published: portBinding.HostPort,
|
||||
Protocol: key.Proto(),
|
||||
HostIP: portBinding.HostIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeHealthCheck(healthConfig *containerType.HealthConfig) *types.HealthCheckConfig {
|
||||
var healthCheck types.HealthCheckConfig
|
||||
healthCheck.Test = healthConfig.Test
|
||||
if healthConfig.Timeout != 0 {
|
||||
timeout := types.Duration(healthConfig.Timeout)
|
||||
healthCheck.Timeout = &timeout
|
||||
}
|
||||
if healthConfig.Interval != 0 {
|
||||
interval := types.Duration(healthConfig.Interval)
|
||||
healthCheck.Interval = &interval
|
||||
}
|
||||
if healthConfig.StartPeriod != 0 {
|
||||
startPeriod := types.Duration(healthConfig.StartPeriod)
|
||||
healthCheck.StartPeriod = &startPeriod
|
||||
}
|
||||
if healthConfig.StartInterval != 0 {
|
||||
startInterval := types.Duration(healthConfig.StartInterval)
|
||||
healthCheck.StartInterval = &startInterval
|
||||
}
|
||||
if healthConfig.Retries != 0 {
|
||||
retries := uint64(healthConfig.Retries)
|
||||
healthCheck.Retries = &retries
|
||||
}
|
||||
return &healthCheck
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeVolumes(volumes []moby.MountPoint) (map[string]types.VolumeConfig,
|
||||
[]types.ServiceVolumeConfig, map[string]types.SecretConfig, []types.ServiceSecretConfig) {
|
||||
volumeConfigs := make(map[string]types.VolumeConfig)
|
||||
secretConfigs := make(map[string]types.SecretConfig)
|
||||
var serviceVolumeConfigs []types.ServiceVolumeConfig
|
||||
var serviceSecretConfigs []types.ServiceSecretConfig
|
||||
|
||||
for _, volume := range volumes {
|
||||
serviceVC := types.ServiceVolumeConfig{
|
||||
Type: string(volume.Type),
|
||||
Source: volume.Source,
|
||||
Target: volume.Destination,
|
||||
ReadOnly: !volume.RW,
|
||||
}
|
||||
switch volume.Type {
|
||||
case mount.TypeVolume:
|
||||
serviceVC.Source = volume.Name
|
||||
vol := types.VolumeConfig{}
|
||||
if volume.Driver != "local" {
|
||||
vol.Driver = volume.Driver
|
||||
vol.Name = volume.Name
|
||||
}
|
||||
volumeConfigs[volume.Name] = vol
|
||||
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
|
||||
case mount.TypeBind:
|
||||
if strings.HasPrefix(volume.Destination, "/run/secrets") {
|
||||
destination := strings.Split(volume.Destination, "/")
|
||||
secret := types.SecretConfig{
|
||||
Name: destination[len(destination)-1],
|
||||
File: strings.TrimPrefix(volume.Source, "/host_mnt"),
|
||||
}
|
||||
secretConfigs[secret.Name] = secret
|
||||
serviceSecretConfigs = append(serviceSecretConfigs, types.ServiceSecretConfig{
|
||||
Source: secret.Name,
|
||||
Target: volume.Destination,
|
||||
})
|
||||
} else {
|
||||
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
|
||||
}
|
||||
}
|
||||
}
|
||||
return volumeConfigs, serviceVolumeConfigs, secretConfigs, serviceSecretConfigs
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeNetwork(networks map[string]*network.EndpointSettings) (map[string]types.NetworkConfig, map[string]*types.ServiceNetworkConfig) {
|
||||
networkConfigs := make(map[string]types.NetworkConfig)
|
||||
serviceNetworkConfigs := make(map[string]*types.ServiceNetworkConfig)
|
||||
|
||||
for name, net := range networks {
|
||||
inspect, err := s.apiClient().NetworkInspect(context.Background(), name, network.InspectOptions{})
|
||||
if err != nil {
|
||||
networkConfigs[name] = types.NetworkConfig{}
|
||||
} else {
|
||||
networkConfigs[name] = types.NetworkConfig{
|
||||
Internal: inspect.Internal,
|
||||
}
|
||||
|
||||
}
|
||||
serviceNetworkConfigs[name] = &types.ServiceNetworkConfig{
|
||||
Aliases: net.Aliases,
|
||||
}
|
||||
}
|
||||
return networkConfigs, serviceNetworkConfigs
|
||||
}
|
||||
|
||||
func cleanDockerPreviousLabels(labels types.Labels) types.Labels {
|
||||
cleanedLabels := types.Labels{}
|
||||
for key, value := range labels {
|
||||
if !strings.HasPrefix(key, "com.docker.compose.") && !strings.HasPrefix(key, "desktop.docker.io") {
|
||||
cleanedLabels[key] = value
|
||||
}
|
||||
}
|
||||
return cleanedLabels
|
||||
}
|
||||
@@ -33,6 +33,7 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
|
||||
o.Deploy.Replicas = nil
|
||||
}
|
||||
o.DependsOn = nil
|
||||
o.Profiles = nil
|
||||
|
||||
bytes, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
|
||||
122
pkg/compose/hook.go
Normal file
122
pkg/compose/hook.go
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
)
|
||||
|
||||
func (s composeService) runHook(ctx context.Context, container moby.Container, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error {
|
||||
wOut := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.HookEventLog,
|
||||
Container: getContainerNameWithoutProject(container) + " ->",
|
||||
ID: container.ID,
|
||||
Service: service.Name,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
defer wOut.Close() //nolint:errcheck
|
||||
|
||||
detached := listener == nil
|
||||
exec, err := s.apiClient().ContainerExecCreate(ctx, container.ID, containerType.ExecOptions{
|
||||
User: hook.User,
|
||||
Privileged: hook.Privileged,
|
||||
Env: ToMobyEnv(hook.Environment),
|
||||
WorkingDir: hook.WorkingDir,
|
||||
Cmd: hook.Command,
|
||||
Detach: detached,
|
||||
AttachStdout: !detached,
|
||||
AttachStderr: !detached,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if detached {
|
||||
return s.runWaitExec(ctx, exec, service, listener)
|
||||
}
|
||||
|
||||
height, width := s.stdout().GetTtySize()
|
||||
consoleSize := &[2]uint{height, width}
|
||||
attach, err := s.apiClient().ContainerExecAttach(ctx, exec.ID, containerType.ExecAttachOptions{
|
||||
Tty: service.Tty,
|
||||
ConsoleSize: consoleSize,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer attach.Close()
|
||||
|
||||
if service.Tty {
|
||||
_, err = io.Copy(wOut, attach.Reader)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(wOut, wOut, attach.Reader)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inspected, err := s.apiClient().ContainerExecInspect(ctx, exec.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inspected.ExitCode != 0 {
|
||||
return fmt.Errorf("%s hook exited with status %d", service.Name, inspected.ExitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s composeService) runWaitExec(ctx context.Context, exec moby.IDResponse, service types.ServiceConfig, listener api.ContainerEventListener) error {
|
||||
err := s.apiClient().ContainerExecStart(ctx, exec.ID, containerType.ExecStartOptions{
|
||||
Detach: listener == nil,
|
||||
Tty: service.Tty,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We miss a ContainerExecWait API
|
||||
tick := time.NewTicker(100 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-tick.C:
|
||||
inspect, err := s.apiClient().ContainerExecInspect(ctx, exec.ID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !inspect.Running {
|
||||
if inspect.ExitCode != 0 {
|
||||
return fmt.Errorf("%s hook exited with status %d", service.Name, inspect.ExitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() er
|
||||
case api.UserCancel:
|
||||
aborting = true
|
||||
case api.ContainerEventAttach:
|
||||
if _, ok := containers[id]; ok {
|
||||
if attached, ok := containers[id]; ok && attached {
|
||||
continue
|
||||
}
|
||||
containers[id] = true
|
||||
@@ -148,7 +148,7 @@ func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() er
|
||||
// Last container terminated, done
|
||||
return exitCode, nil
|
||||
}
|
||||
case api.ContainerEventLog:
|
||||
case api.ContainerEventLog, api.HookEventLog:
|
||||
if !aborting {
|
||||
p.consumer.Log(container, event.Line)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re
|
||||
}
|
||||
|
||||
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
|
||||
err := s.Push(ctx, project, api.PushOptions{})
|
||||
err := s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
w := progress.ContextWriter(ctx)
|
||||
for _, service := range project.Services {
|
||||
if service.Build == nil || service.Image == "" {
|
||||
if options.ImageMandatory && service.Image == "" {
|
||||
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
|
||||
}
|
||||
w.Event(progress.Event{
|
||||
ID: service.Name,
|
||||
Status: progress.Done,
|
||||
|
||||
@@ -93,7 +93,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
}
|
||||
|
||||
if !opts.NoDeps {
|
||||
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState); err != nil {
|
||||
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
return "", err
|
||||
}
|
||||
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts)
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
return err
|
||||
}
|
||||
|
||||
return s.startService(ctx, project, service, containers)
|
||||
return s.startService(ctx, project, service, containers, listener, options.WaitTimeout)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -149,7 +149,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
err = s.waitDependencies(ctx, project, project.Name, depends, containers)
|
||||
err = s.waitDependencies(ctx, project, project.Name, depends, containers, 0)
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return fmt.Errorf("application not healthy after %s", options.WaitTimeout)
|
||||
@@ -214,13 +214,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}
|
||||
|
||||
var (
|
||||
expected []string
|
||||
expected = utils.NewSet[string]()
|
||||
watched = map[string]int{}
|
||||
replaced []string
|
||||
)
|
||||
for _, c := range containers {
|
||||
if isRequired(c) {
|
||||
expected = append(expected, c.ID)
|
||||
expected.Add(c.ID)
|
||||
}
|
||||
watched[c.ID] = 0
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
// be able to inspect in time before they're gone from the
|
||||
// API, so just remove the watch without erroring
|
||||
delete(watched, event.Container)
|
||||
expected = utils.Remove(expected, event.Container)
|
||||
expected.Remove(event.Container)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -253,7 +253,6 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
Labels: inspected.Config.Labels,
|
||||
}
|
||||
name := getContainerNameWithoutProject(container)
|
||||
|
||||
service := container.Labels[api.ServiceLabel]
|
||||
switch event.Status {
|
||||
case "stop":
|
||||
@@ -278,7 +277,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}
|
||||
|
||||
delete(watched, container.ID)
|
||||
expected = utils.Remove(expected, container.ID)
|
||||
expected.Remove(container.ID)
|
||||
case "die":
|
||||
restarted := watched[container.ID]
|
||||
watched[container.ID] = restarted + 1
|
||||
@@ -308,7 +307,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
if !willRestart {
|
||||
// we're done with this one
|
||||
delete(watched, container.ID)
|
||||
expected = utils.Remove(expected, container.ID)
|
||||
expected.Remove(container.ID)
|
||||
}
|
||||
case "start":
|
||||
count, ok := watched[container.ID]
|
||||
@@ -316,7 +315,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
if !ok {
|
||||
// A new container has just been added to service by scale
|
||||
watched[container.ID] = 0
|
||||
expected = append(expected, container.ID)
|
||||
expected.Add(container.ID)
|
||||
mustAttach = true
|
||||
}
|
||||
if mustAttach {
|
||||
@@ -333,17 +332,15 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if utils.StringContains(expected, id) {
|
||||
expected = append(expected, inspected.ID)
|
||||
if expected.Has(id) {
|
||||
expected.Add(inspected.ID)
|
||||
expected.Add(container.ID)
|
||||
}
|
||||
watched[container.ID] = 1
|
||||
if utils.Contains(expected, id) {
|
||||
expected = append(expected, container.ID)
|
||||
}
|
||||
} else if ofInterest(container) {
|
||||
watched[container.ID] = 1
|
||||
if isRequired(container) {
|
||||
expected = append(expected, container.ID)
|
||||
expected.Add(container.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
|
||||
if !utils.StringContains(options.Services, service) {
|
||||
return nil
|
||||
}
|
||||
return s.stopContainers(ctx, w, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
|
||||
serv := project.Services[service]
|
||||
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,10 +98,9 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
defer keyboard.Close() //nolint:errcheck
|
||||
isWatchConfigured := s.shouldWatch(project)
|
||||
isDockerDesktopActive := s.isDesktopIntegrationActive()
|
||||
isDockerDesktopComposeUI := s.isDesktopUIEnabled()
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI)
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
|
||||
|
||||
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI, signalChan, s.watch)
|
||||
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.watch)
|
||||
if options.Start.Watch {
|
||||
formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
project.Services[i] = service
|
||||
|
||||
dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
|
||||
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func maybeFileEvent(trigger types.Trigger, hostPath string, ignore watch.PathMat
|
||||
return nil
|
||||
}
|
||||
// always use Unix-style paths for inside the container
|
||||
containerPath = path.Join(trigger.Target, rel)
|
||||
containerPath = path.Join(trigger.Target, filepath.ToSlash(rel))
|
||||
}
|
||||
|
||||
return &fileEvent{
|
||||
@@ -512,9 +512,15 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
|
||||
services := []string{serviceName}
|
||||
p, err := project.WithSelectedServices(services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.start(ctx, project.Name, api.StartOptions{
|
||||
Project: project,
|
||||
Services: []string{serviceName},
|
||||
Project: p,
|
||||
Services: services,
|
||||
AttachTo: services,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Application failed to start after update. Error: %v", err))
|
||||
@@ -527,7 +533,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
|
||||
pathMappings[i] = batch[i].PathMapping
|
||||
}
|
||||
|
||||
writeWatchSyncMessage(options.LogTo, serviceName, pathMappings)
|
||||
writeWatchSyncMessage(options.LogTo, serviceName, pathMappings, restartService)
|
||||
|
||||
service, err := project.GetService(serviceName)
|
||||
if err != nil {
|
||||
@@ -537,17 +543,28 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
if restartService {
|
||||
return s.Restart(ctx, project.Name, api.RestartOptions{
|
||||
err = s.restart(ctx, project.Name, api.RestartOptions{
|
||||
Services: []string{serviceName},
|
||||
Project: project,
|
||||
NoDeps: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.LogTo.Log(
|
||||
api.WatchLogger,
|
||||
fmt.Sprintf("service %q restarted", serviceName))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeWatchSyncMessage prints out a message about the sync for the changed paths.
|
||||
func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping) {
|
||||
func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping, restart bool) {
|
||||
action := "Syncing"
|
||||
if restart {
|
||||
action = "Syncing and restarting"
|
||||
}
|
||||
if logrus.IsLevelEnabled(logrus.DebugLevel) {
|
||||
hostPathsToSync := make([]string, len(pathMappings))
|
||||
for i := range pathMappings {
|
||||
@@ -556,7 +573,8 @@ func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings
|
||||
log.Log(
|
||||
api.WatchLogger,
|
||||
fmt.Sprintf(
|
||||
"Syncing %q after changes were detected: %s",
|
||||
"%s service %q after changes were detected: %s",
|
||||
action,
|
||||
serviceName,
|
||||
strings.Join(hostPathsToSync, ", "),
|
||||
),
|
||||
@@ -564,7 +582,7 @@ func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings
|
||||
} else {
|
||||
log.Log(
|
||||
api.WatchLogger,
|
||||
fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings)),
|
||||
fmt.Sprintf("%s service %q after %d changes were detected", action, serviceName, len(pathMappings)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,32 @@ func TestLocalComposeExec(t *testing.T) {
|
||||
assert.Check(t, !strings.Contains(res.Stdout(), "FOO="), res.Combined())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalComposeExecOneOff(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
const projectName = "compose-e2e-exec-one-off"
|
||||
cmdArgs := func(cmd string, args ...string) []string {
|
||||
ret := []string{"--project-directory", "fixtures/simple-composefile", "--project-name", projectName, cmd}
|
||||
ret = append(ret, args...)
|
||||
return ret
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
c.RunDockerComposeCmd(t, cmdArgs("down", "--timeout=0")...)
|
||||
}
|
||||
cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
c.RunDockerComposeCmd(t, cmdArgs("run", "-d", "simple")...)
|
||||
|
||||
t.Run("exec in one-off container", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, cmdArgs("exec", "-e", "FOO", "simple", "/usr/bin/env")...)
|
||||
assert.Check(t, !strings.Contains(res.Stdout(), "FOO="), res.Combined())
|
||||
})
|
||||
|
||||
t.Run("exec with index", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, cmdArgs("exec", "--index", "1", "-e", "FOO", "simple", "/usr/bin/env")...)
|
||||
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "service \"simple\" is not running container #1"})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
assert.Assert(t, runContainerID != "")
|
||||
res = c.RunDockerCmd(t, "inspect", runContainerID)
|
||||
res.Assert(t, icmd.Expected{Out: ` "Status": "exited"`})
|
||||
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.container-number": "1"`})
|
||||
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "run-test"`})
|
||||
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "True",`})
|
||||
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.slug": "` + truncatedSlug})
|
||||
|
||||
31
pkg/e2e/env_file_test.go
Normal file
31
pkg/e2e/env_file_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestRawEnvFile(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dotenv/raw.yaml", "run", "test")
|
||||
assert.Equal(t, strings.TrimSpace(res.Stdout()), "'{\"key\": \"value\"}'")
|
||||
}
|
||||
50
pkg/e2e/export_test.go
Normal file
50
pkg/e2e/export_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
const projectName = "e2e-export-service"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
cleanup := func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
cleanup()
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service")
|
||||
}
|
||||
|
||||
func TestExportWithReplicas(t *testing.T) {
|
||||
const projectName = "e2e-export-service-with-replicas"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
cleanup := func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
cleanup()
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas")
|
||||
}
|
||||
1
pkg/e2e/fixtures/dotenv/.env.raw
Normal file
1
pkg/e2e/fixtures/dotenv/.env.raw
Normal file
@@ -0,0 +1 @@
|
||||
TEST_VAR='{"key": "value"}'
|
||||
7
pkg/e2e/fixtures/dotenv/raw.yaml
Normal file
7
pkg/e2e/fixtures/dotenv/raw.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
test:
|
||||
image: alpine
|
||||
command: sh -c "echo $$TEST_VAR"
|
||||
env_file:
|
||||
- path: .env.raw
|
||||
format: raw # parse without interpolation
|
||||
9
pkg/e2e/fixtures/export/compose.yaml
Normal file
9
pkg/e2e/fixtures/export/compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
service:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
service-with-replicas:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
deploy:
|
||||
replicas: 3
|
||||
15
pkg/e2e/fixtures/profiles/docker-compose.yaml
Normal file
15
pkg/e2e/fixtures/profiles/docker-compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
foo:
|
||||
container_name: foo_c
|
||||
profiles: [ test ]
|
||||
image: alpine
|
||||
depends_on: [ db ]
|
||||
|
||||
bar:
|
||||
container_name: bar_c
|
||||
profiles: [ test ]
|
||||
image: alpine
|
||||
|
||||
db:
|
||||
container_name: db_c
|
||||
image: alpine
|
||||
@@ -179,3 +179,16 @@ func TestUpWithAllResources(t *testing.T) {
|
||||
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume "%s_my_vol" Created`, projectName)), res.Combined())
|
||||
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined())
|
||||
}
|
||||
|
||||
func TestUpProfile(t *testing.T) {
|
||||
c := NewCLI(t)
|
||||
const projectName = "compose-e2e-up-profile"
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "--profile", "test", "down", "-v")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/docker-compose.yaml", "--project-name", projectName, "up", "foo")
|
||||
assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined())
|
||||
assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined())
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined())
|
||||
}
|
||||
|
||||
@@ -99,9 +99,8 @@ func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
|
||||
errors)
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
|
||||
n := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
|
||||
pn := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
|
||||
assert.Equal(t, pn.Stdout(), n.Stdout())
|
||||
assert.Equal(t, strings.TrimSpace(pn.Stdout()), networkName)
|
||||
|
||||
t.Log("create a dotenv file that will be used to trigger the rebuild")
|
||||
err = os.WriteFile(dotEnvFilepath, []byte("HELLO=WORLD\nTEST=REBUILD"), 0o666)
|
||||
@@ -119,9 +118,8 @@ func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
|
||||
return true, fmt.Sprintf("container %s was rebuilt", containerName)
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
|
||||
n2 := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
|
||||
pn2 := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
|
||||
assert.Equal(t, pn2.Stdout(), n2.Stdout())
|
||||
assert.Equal(t, strings.TrimSpace(pn2.Stdout()), networkName)
|
||||
|
||||
assert.Check(t, !strings.Contains(r.Combined(), "Application failed to start after update"))
|
||||
|
||||
@@ -290,8 +288,7 @@ func doTest(t *testing.T, svcName string) {
|
||||
return poll.Continue("%v", r.Combined())
|
||||
}
|
||||
}
|
||||
poll.WaitOn(t, checkRestart(fmt.Sprintf("%s-1 Restarting", svcName)))
|
||||
poll.WaitOn(t, checkRestart(fmt.Sprintf("%s-1 Started", svcName)))
|
||||
poll.WaitOn(t, checkRestart(fmt.Sprintf("service %q restarted", svcName)))
|
||||
poll.WaitOn(t, checkFileContents("/app/config/file.config", "This is an updated config file"))
|
||||
|
||||
testComplete.Store(true)
|
||||
|
||||
@@ -155,6 +155,35 @@ func (mr *MockServiceMockRecorder) Exec(ctx, projectName, options any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockService)(nil).Exec), ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Export mocks base method.
|
||||
func (m *MockService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Export", ctx, projectName, options)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Export indicates an expected call of Export.
|
||||
func (mr *MockServiceMockRecorder) Export(ctx, projectName, options any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockService)(nil).Export), ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Generate mocks base method.
|
||||
func (m *MockService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Generate", ctx, options)
|
||||
ret0, _ := ret[0].(*types.Project)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Generate indicates an expected call of Generate.
|
||||
func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockService)(nil).Generate), ctx, options)
|
||||
}
|
||||
|
||||
// Images mocks base method.
|
||||
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/docker/buildx/store/storeutil"
|
||||
"github.com/docker/buildx/util/imagetools"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/internal/ocipush"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
@@ -113,6 +114,8 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
|
||||
|
||||
err2 := g.pullComposeFiles(ctx, local, composeFile, manifest, ref, resolver)
|
||||
if err2 != nil {
|
||||
// we need to clean up the directory to be sure we won't let empty files present
|
||||
_ = os.RemoveAll(local)
|
||||
return "", err2
|
||||
}
|
||||
}
|
||||
@@ -137,8 +140,8 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, com
|
||||
return err
|
||||
}
|
||||
defer f.Close() //nolint:errcheck
|
||||
|
||||
if manifest.ArtifactType != "application/vnd.docker.compose.project" {
|
||||
if (manifest.ArtifactType != "" && manifest.ArtifactType != ocipush.ComposeProjectArtifactType) ||
|
||||
(manifest.ArtifactType == "" && manifest.Config.MediaType != ocipush.ComposeEmptyConfigMediaType) {
|
||||
return fmt.Errorf("%s is not a compose project OCI artifact, but %s", ref.String(), manifest.ArtifactType)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/internal/paths"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/moby/patternmatcher/ignorefile"
|
||||
@@ -61,13 +64,28 @@ func (i dockerPathMatcher) MatchesEntireDir(f string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func LoadDockerIgnore(repoRoot string) (*dockerPathMatcher, error) {
|
||||
func LoadDockerIgnore(build *types.BuildConfig) (*dockerPathMatcher, error) {
|
||||
repoRoot := build.Context
|
||||
absRoot, err := filepath.Abs(repoRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patterns, err := readDockerignorePatterns(absRoot)
|
||||
// first try Dockerfile-specific ignore-file
|
||||
f, err := os.Open(filepath.Join(repoRoot, build.Dockerfile+".dockerignore"))
|
||||
if os.IsNotExist(err) {
|
||||
// defaults to a global .dockerignore
|
||||
f, err = os.Open(filepath.Join(repoRoot, ".dockerignore"))
|
||||
if os.IsNotExist(err) {
|
||||
return NewDockerPatternMatcher(repoRoot, nil)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
patterns, err := readDockerignorePatterns(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,6 +129,15 @@ func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if "*" is present in patterns
|
||||
hasAllPattern := slices.Contains(patterns, "*")
|
||||
if hasAllPattern {
|
||||
// Remove all non-exclusion patterns (those that don't start with '!')
|
||||
patterns = slices.DeleteFunc(patterns, func(p string) bool {
|
||||
return p != "" && p[0] != '!' // Only keep exclusion patterns
|
||||
})
|
||||
}
|
||||
|
||||
pm, err := patternmatcher.New(absPatterns(absRoot, patterns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -122,19 +149,8 @@ func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMat
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readDockerignorePatterns(repoRoot string) ([]string, error) {
|
||||
var excludes []string
|
||||
|
||||
f, err := os.Open(filepath.Join(repoRoot, ".dockerignore"))
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return excludes, nil
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
patterns, err := ignorefile.ReadAll(f)
|
||||
func readDockerignorePatterns(r io.Reader) ([]string, error) {
|
||||
patterns, err := ignorefile.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
|
||||
}
|
||||
|
||||
108
pkg/watch/dockerignore_test.go
Normal file
108
pkg/watch/dockerignore_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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 watch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDockerPatternMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repoRoot string
|
||||
patterns []string
|
||||
expectedErr bool
|
||||
expectedRoot string
|
||||
expectedPat []string
|
||||
}{
|
||||
{
|
||||
name: "Basic patterns without wildcard",
|
||||
repoRoot: "/repo",
|
||||
patterns: []string{"dir1/", "file.txt"},
|
||||
expectedErr: false,
|
||||
expectedRoot: "/repo",
|
||||
expectedPat: []string{"/repo/dir1", "/repo/file.txt"},
|
||||
},
|
||||
{
|
||||
name: "Patterns with exclusion",
|
||||
repoRoot: "/repo",
|
||||
patterns: []string{"dir1/", "!file.txt"},
|
||||
expectedErr: false,
|
||||
expectedRoot: "/repo",
|
||||
expectedPat: []string{"/repo/dir1", "!/repo/file.txt"},
|
||||
},
|
||||
{
|
||||
name: "Wildcard with exclusion",
|
||||
repoRoot: "/repo",
|
||||
patterns: []string{"*", "!file.txt"},
|
||||
expectedErr: false,
|
||||
expectedRoot: "/repo",
|
||||
expectedPat: []string{"!/repo/file.txt"},
|
||||
},
|
||||
{
|
||||
name: "No patterns",
|
||||
repoRoot: "/repo",
|
||||
patterns: []string{},
|
||||
expectedErr: false,
|
||||
expectedRoot: "/repo",
|
||||
expectedPat: nil,
|
||||
},
|
||||
{
|
||||
name: "Only exclusion pattern",
|
||||
repoRoot: "/repo",
|
||||
patterns: []string{"!file.txt"},
|
||||
expectedErr: false,
|
||||
expectedRoot: "/repo",
|
||||
expectedPat: []string{"!/repo/file.txt"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Call the function with the test data
|
||||
matcher, err := NewDockerPatternMatcher(tt.repoRoot, tt.patterns)
|
||||
|
||||
// Check if we expect an error
|
||||
if (err != nil) != tt.expectedErr {
|
||||
t.Fatalf("expected error: %v, got: %v", tt.expectedErr, err)
|
||||
}
|
||||
|
||||
// If no error is expected, check the output
|
||||
if !tt.expectedErr {
|
||||
if matcher.repoRoot != tt.expectedRoot {
|
||||
t.Errorf("expected root: %v, got: %v", tt.expectedRoot, matcher.repoRoot)
|
||||
}
|
||||
|
||||
// Compare patterns
|
||||
actualPatterns := matcher.matcher.Patterns()
|
||||
if len(actualPatterns) != len(tt.expectedPat) {
|
||||
t.Errorf("expected patterns length: %v, got: %v", len(tt.expectedPat), len(actualPatterns))
|
||||
}
|
||||
|
||||
for i, expectedPat := range tt.expectedPat {
|
||||
actualPatternStr := actualPatterns[i].String()
|
||||
if actualPatterns[i].Exclusion() {
|
||||
actualPatternStr = "!" + actualPatternStr
|
||||
}
|
||||
if actualPatternStr != expectedPat {
|
||||
t.Errorf("expected pattern: %v, got: %v", expectedPat, actualPatterns[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user