Repository: docker/compose
Branch: main
Commit: b043368028e9
Files: 650
Total size: 1.8 MB
Directory structure:
gitextract_6bdxpuyg/
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yaml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SECURITY.md
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── ci.yml
│ ├── docs-upstream.yml
│ ├── merge.yml
│ ├── scorecards.yml
│ └── stale.yml
├── .gitignore
├── .go-version
├── .golangci.yml
├── BUILDING.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── cmd/
│ ├── cmdtrace/
│ │ ├── cmd_span.go
│ │ └── cmd_span_test.go
│ ├── compatibility/
│ │ ├── convert.go
│ │ └── convert_test.go
│ ├── compose/
│ │ ├── alpha.go
│ │ ├── attach.go
│ │ ├── bridge.go
│ │ ├── build.go
│ │ ├── commit.go
│ │ ├── completion.go
│ │ ├── compose.go
│ │ ├── compose_oci_test.go
│ │ ├── compose_test.go
│ │ ├── config.go
│ │ ├── cp.go
│ │ ├── create.go
│ │ ├── down.go
│ │ ├── events.go
│ │ ├── exec.go
│ │ ├── export.go
│ │ ├── generate.go
│ │ ├── images.go
│ │ ├── kill.go
│ │ ├── list.go
│ │ ├── logs.go
│ │ ├── options.go
│ │ ├── options_test.go
│ │ ├── pause.go
│ │ ├── port.go
│ │ ├── ps.go
│ │ ├── publish.go
│ │ ├── pull.go
│ │ ├── pullOptions_test.go
│ │ ├── push.go
│ │ ├── remove.go
│ │ ├── restart.go
│ │ ├── run.go
│ │ ├── scale.go
│ │ ├── start.go
│ │ ├── stats.go
│ │ ├── stop.go
│ │ ├── top.go
│ │ ├── top_test.go
│ │ ├── up.go
│ │ ├── up_test.go
│ │ ├── version.go
│ │ ├── version_test.go
│ │ ├── viz.go
│ │ ├── viz_test.go
│ │ ├── volumes.go
│ │ ├── wait.go
│ │ └── watch.go
│ ├── display/
│ │ ├── colors.go
│ │ ├── dryrun.go
│ │ ├── json.go
│ │ ├── json_test.go
│ │ ├── mode.go
│ │ ├── plain.go
│ │ ├── quiet.go
│ │ ├── spinner.go
│ │ ├── tty.go
│ │ └── tty_test.go
│ ├── formatter/
│ │ ├── ansi.go
│ │ ├── colors.go
│ │ ├── consts.go
│ │ ├── container.go
│ │ ├── formatter.go
│ │ ├── formatter_test.go
│ │ ├── json.go
│ │ ├── logs.go
│ │ ├── pretty.go
│ │ ├── shortcut.go
│ │ ├── shortcut_unix.go
│ │ └── shortcut_windows.go
│ ├── main.go
│ └── prompt/
│ ├── prompt.go
│ └── prompt_mock.go
├── codecov.yml
├── docker-bake.hcl
├── docs/
│ ├── examples/
│ │ └── provider.go
│ ├── extension.md
│ ├── reference/
│ │ ├── compose.md
│ │ ├── compose_alpha.md
│ │ ├── compose_alpha_dry-run.md
│ │ ├── compose_alpha_generate.md
│ │ ├── compose_alpha_publish.md
│ │ ├── compose_alpha_scale.md
│ │ ├── compose_alpha_viz.md
│ │ ├── compose_alpha_watch.md
│ │ ├── compose_attach.md
│ │ ├── compose_bridge.md
│ │ ├── compose_bridge_convert.md
│ │ ├── compose_bridge_transformations.md
│ │ ├── compose_bridge_transformations_create.md
│ │ ├── compose_bridge_transformations_list.md
│ │ ├── compose_build.md
│ │ ├── compose_commit.md
│ │ ├── compose_config.md
│ │ ├── compose_cp.md
│ │ ├── compose_create.md
│ │ ├── compose_down.md
│ │ ├── compose_events.md
│ │ ├── compose_exec.md
│ │ ├── compose_export.md
│ │ ├── compose_images.md
│ │ ├── compose_kill.md
│ │ ├── compose_logs.md
│ │ ├── compose_ls.md
│ │ ├── compose_pause.md
│ │ ├── compose_port.md
│ │ ├── compose_ps.md
│ │ ├── compose_publish.md
│ │ ├── compose_pull.md
│ │ ├── compose_push.md
│ │ ├── compose_restart.md
│ │ ├── compose_rm.md
│ │ ├── compose_run.md
│ │ ├── compose_scale.md
│ │ ├── compose_start.md
│ │ ├── compose_stats.md
│ │ ├── compose_stop.md
│ │ ├── compose_top.md
│ │ ├── compose_unpause.md
│ │ ├── compose_up.md
│ │ ├── compose_version.md
│ │ ├── compose_volumes.md
│ │ ├── compose_wait.md
│ │ ├── compose_watch.md
│ │ ├── docker_compose.yaml
│ │ ├── docker_compose_alpha.yaml
│ │ ├── docker_compose_alpha_dry-run.yaml
│ │ ├── docker_compose_alpha_generate.yaml
│ │ ├── docker_compose_alpha_publish.yaml
│ │ ├── docker_compose_alpha_scale.yaml
│ │ ├── docker_compose_alpha_viz.yaml
│ │ ├── docker_compose_alpha_watch.yaml
│ │ ├── docker_compose_attach.yaml
│ │ ├── docker_compose_bridge.yaml
│ │ ├── docker_compose_bridge_convert.yaml
│ │ ├── docker_compose_bridge_transformations.yaml
│ │ ├── docker_compose_bridge_transformations_create.yaml
│ │ ├── docker_compose_bridge_transformations_list.yaml
│ │ ├── docker_compose_build.yaml
│ │ ├── docker_compose_commit.yaml
│ │ ├── docker_compose_config.yaml
│ │ ├── docker_compose_convert.yaml
│ │ ├── docker_compose_cp.yaml
│ │ ├── docker_compose_create.yaml
│ │ ├── 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
│ │ ├── docker_compose_ls.yaml
│ │ ├── docker_compose_pause.yaml
│ │ ├── docker_compose_port.yaml
│ │ ├── docker_compose_ps.yaml
│ │ ├── docker_compose_publish.yaml
│ │ ├── docker_compose_pull.yaml
│ │ ├── docker_compose_push.yaml
│ │ ├── docker_compose_restart.yaml
│ │ ├── docker_compose_rm.yaml
│ │ ├── docker_compose_run.yaml
│ │ ├── docker_compose_scale.yaml
│ │ ├── docker_compose_start.yaml
│ │ ├── docker_compose_stats.yaml
│ │ ├── docker_compose_stop.yaml
│ │ ├── docker_compose_top.yaml
│ │ ├── docker_compose_unpause.yaml
│ │ ├── docker_compose_up.yaml
│ │ ├── docker_compose_version.yaml
│ │ ├── docker_compose_volumes.yaml
│ │ ├── docker_compose_wait.yaml
│ │ └── docker_compose_watch.yaml
│ ├── sdk.md
│ └── yaml/
│ └── main/
│ └── generate.go
├── go.mod
├── go.sum
├── internal/
│ ├── desktop/
│ │ ├── client.go
│ │ └── client_test.go
│ ├── experimental/
│ │ └── experimental.go
│ ├── locker/
│ │ ├── pidfile.go
│ │ ├── pidfile_unix.go
│ │ ├── pidfile_windows.go
│ │ ├── runtime.go
│ │ ├── runtime_darwin.go
│ │ ├── runtime_unix.go
│ │ └── runtime_windows.go
│ ├── memnet/
│ │ ├── conn.go
│ │ ├── conn_unix.go
│ │ └── conn_windows.go
│ ├── oci/
│ │ ├── push.go
│ │ └── resolver.go
│ ├── paths/
│ │ └── paths.go
│ ├── registry/
│ │ └── registry.go
│ ├── sync/
│ │ ├── shared.go
│ │ └── tar.go
│ ├── tracing/
│ │ ├── attributes.go
│ │ ├── attributes_test.go
│ │ ├── docker_context.go
│ │ ├── errors.go
│ │ ├── keyboard_metrics.go
│ │ ├── mux.go
│ │ ├── tracing.go
│ │ ├── tracing_test.go
│ │ └── wrap.go
│ └── variables.go
└── pkg/
├── api/
│ ├── api.go
│ ├── api_test.go
│ ├── context.go
│ ├── env.go
│ ├── errors.go
│ ├── errors_test.go
│ ├── event.go
│ ├── labels.go
│ └── labels_test.go
├── bridge/
│ ├── convert.go
│ └── transformers.go
├── compose/
│ ├── apiSocket.go
│ ├── api_versions.go
│ ├── attach.go
│ ├── attach_service.go
│ ├── build.go
│ ├── build_bake.go
│ ├── build_classic.go
│ ├── build_test.go
│ ├── commit.go
│ ├── compose.go
│ ├── container.go
│ ├── containers.go
│ ├── convergence.go
│ ├── convergence_test.go
│ ├── convert.go
│ ├── cp.go
│ ├── create.go
│ ├── create_test.go
│ ├── dependencies.go
│ ├── dependencies_test.go
│ ├── desktop.go
│ ├── docker_cli_providers.go
│ ├── down.go
│ ├── down_test.go
│ ├── envresolver.go
│ ├── envresolver_test.go
│ ├── events.go
│ ├── exec.go
│ ├── export.go
│ ├── filters.go
│ ├── generate.go
│ ├── hash.go
│ ├── hash_test.go
│ ├── hook.go
│ ├── hook_test.go
│ ├── image_pruner.go
│ ├── images.go
│ ├── images_test.go
│ ├── kill.go
│ ├── kill_test.go
│ ├── loader.go
│ ├── loader_test.go
│ ├── logs.go
│ ├── logs_test.go
│ ├── ls.go
│ ├── ls_test.go
│ ├── model.go
│ ├── monitor.go
│ ├── pause.go
│ ├── plugins.go
│ ├── plugins_windows.go
│ ├── port.go
│ ├── printer.go
│ ├── progress.go
│ ├── ps.go
│ ├── ps_test.go
│ ├── publish.go
│ ├── publish_test.go
│ ├── pull.go
│ ├── push.go
│ ├── remove.go
│ ├── restart.go
│ ├── run.go
│ ├── scale.go
│ ├── secrets.go
│ ├── shellout.go
│ ├── start.go
│ ├── stop.go
│ ├── stop_test.go
│ ├── suffix_unix.go
│ ├── testdata/
│ │ ├── compose.yaml
│ │ └── publish/
│ │ ├── common.yaml
│ │ ├── compose.yaml
│ │ └── test.env
│ ├── top.go
│ ├── transform/
│ │ ├── replace.go
│ │ └── replace_test.go
│ ├── up.go
│ ├── viz.go
│ ├── viz_test.go
│ ├── volumes.go
│ ├── volumes_test.go
│ ├── wait.go
│ ├── watch.go
│ └── watch_test.go
├── dryrun/
│ └── dryrunclient.go
├── e2e/
│ ├── assert.go
│ ├── bridge_test.go
│ ├── build_test.go
│ ├── cancel_test.go
│ ├── cascade_test.go
│ ├── commit_test.go
│ ├── compose_environment_test.go
│ ├── compose_exec_test.go
│ ├── compose_run_build_once_test.go
│ ├── compose_run_test.go
│ ├── compose_test.go
│ ├── compose_up_test.go
│ ├── config_test.go
│ ├── configs_test.go
│ ├── container_name_test.go
│ ├── cp_test.go
│ ├── e2e_config_plugin.go
│ ├── e2e_config_standalone.go
│ ├── env_file_test.go
│ ├── exec_test.go
│ ├── export_test.go
│ ├── expose_test.go
│ ├── fixtures/
│ │ ├── attach-restart/
│ │ │ └── compose.yaml
│ │ ├── bridge/
│ │ │ ├── Dockerfile
│ │ │ ├── compose.yaml
│ │ │ ├── expected-helm/
│ │ │ │ ├── Chart.yaml
│ │ │ │ ├── templates/
│ │ │ │ │ ├── 0-bridge-namespace.yaml
│ │ │ │ │ ├── bridge-configs.yaml
│ │ │ │ │ ├── my-secrets-secret.yaml
│ │ │ │ │ ├── private-network-network-policy.yaml
│ │ │ │ │ ├── public-network-network-policy.yaml
│ │ │ │ │ ├── serviceA-deployment.yaml
│ │ │ │ │ ├── serviceA-expose.yaml
│ │ │ │ │ ├── serviceA-service.yaml
│ │ │ │ │ ├── serviceB-deployment.yaml
│ │ │ │ │ ├── serviceB-expose.yaml
│ │ │ │ │ └── serviceB-service.yaml
│ │ │ │ └── values.yaml
│ │ │ ├── expected-kubernetes/
│ │ │ │ ├── base/
│ │ │ │ │ ├── 0-bridge-namespace.yaml
│ │ │ │ │ ├── bridge-configs.yaml
│ │ │ │ │ ├── kustomization.yaml
│ │ │ │ │ ├── my-secrets-secret.yaml
│ │ │ │ │ ├── private-network-network-policy.yaml
│ │ │ │ │ ├── public-network-network-policy.yaml
│ │ │ │ │ ├── serviceA-deployment.yaml
│ │ │ │ │ ├── serviceA-expose.yaml
│ │ │ │ │ ├── serviceA-service.yaml
│ │ │ │ │ ├── serviceB-deployment.yaml
│ │ │ │ │ ├── serviceB-expose.yaml
│ │ │ │ │ └── serviceB-service.yaml
│ │ │ │ └── overlays/
│ │ │ │ └── desktop/
│ │ │ │ ├── kustomization.yaml
│ │ │ │ ├── serviceA-service.yaml
│ │ │ │ └── serviceB-service.yaml
│ │ │ ├── my-config.txt
│ │ │ └── not-so-secret.txt
│ │ ├── build-dependencies/
│ │ │ ├── base.dockerfile
│ │ │ ├── classic.yaml
│ │ │ ├── compose-depends_on.yaml
│ │ │ ├── compose.yaml
│ │ │ ├── hello.txt
│ │ │ └── service.dockerfile
│ │ ├── build-infinite/
│ │ │ ├── compose.yaml
│ │ │ └── service1/
│ │ │ └── Dockerfile
│ │ ├── build-test/
│ │ │ ├── compose.yaml
│ │ │ ├── dependencies/
│ │ │ │ └── compose.yaml
│ │ │ ├── entitlements/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── escaped/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── long-output-line/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── minimal/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── multi-args/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── nginx-build/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── static/
│ │ │ │ └── index.html
│ │ │ ├── nginx-build2/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── static2/
│ │ │ │ └── index.html
│ │ │ ├── platforms/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── compose-multiple-platform-builds.yaml
│ │ │ │ ├── compose-service-platform-and-no-build-platforms.yaml
│ │ │ │ ├── compose-service-platform-not-in-build-platforms.yaml
│ │ │ │ ├── compose-unsupported-platform.yml
│ │ │ │ ├── compose.yaml
│ │ │ │ ├── contextServiceA/
│ │ │ │ │ └── Dockerfile
│ │ │ │ ├── contextServiceB/
│ │ │ │ │ └── Dockerfile
│ │ │ │ └── contextServiceC/
│ │ │ │ └── Dockerfile
│ │ │ ├── privileged/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── profiles/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── compose.yaml
│ │ │ │ └── test-secret.txt
│ │ │ ├── secrets/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── compose.yml
│ │ │ │ └── secret.txt
│ │ │ ├── ssh/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── compose-without-ssh.yaml
│ │ │ │ ├── compose.yaml
│ │ │ │ ├── fake_rsa
│ │ │ │ └── fake_rsa.pub
│ │ │ ├── sub-dependencies/
│ │ │ │ └── compose.yaml
│ │ │ ├── subset/
│ │ │ │ └── compose.yaml
│ │ │ └── tags/
│ │ │ ├── Dockerfile
│ │ │ └── compose.yaml
│ │ ├── cascade/
│ │ │ └── compose.yaml
│ │ ├── commit/
│ │ │ └── compose.yaml
│ │ ├── compose-pull/
│ │ │ ├── duplicate-images/
│ │ │ │ └── compose.yaml
│ │ │ ├── image-present-locally/
│ │ │ │ └── compose.yaml
│ │ │ ├── no-image-name-given/
│ │ │ │ └── compose.yaml
│ │ │ ├── simple/
│ │ │ │ └── compose.yaml
│ │ │ └── unknown-image/
│ │ │ ├── Dockerfile
│ │ │ └── compose.yaml
│ │ ├── config/
│ │ │ └── compose.yaml
│ │ ├── configs/
│ │ │ ├── compose.yaml
│ │ │ └── config.txt
│ │ ├── container_name/
│ │ │ └── compose.yaml
│ │ ├── cp-test/
│ │ │ ├── compose.yaml
│ │ │ ├── cp-folder/
│ │ │ │ └── cp-me.txt
│ │ │ └── cp-me.txt
│ │ ├── dependencies/
│ │ │ ├── Dockerfile
│ │ │ ├── compose.yaml
│ │ │ ├── dependency-exit.yaml
│ │ │ ├── deps-completed-successfully.yaml
│ │ │ ├── deps-not-required.yaml
│ │ │ ├── recreate-no-deps.yaml
│ │ │ └── service-image-depends-on.yaml
│ │ ├── dotenv/
│ │ │ ├── development/
│ │ │ │ └── compose.yaml
│ │ │ └── raw.yaml
│ │ ├── env-secret/
│ │ │ ├── child/
│ │ │ │ └── compose.yaml
│ │ │ ├── compose.yaml
│ │ │ └── secret.env
│ │ ├── env_file/
│ │ │ ├── compose.yaml
│ │ │ └── test.env
│ │ ├── environment/
│ │ │ ├── empty-variable/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── env-file-comments/
│ │ │ │ ├── Dockerfile
│ │ │ │ └── compose.yaml
│ │ │ ├── env-interpolation/
│ │ │ │ └── compose.yaml
│ │ │ ├── env-interpolation-default-value/
│ │ │ │ └── compose.yaml
│ │ │ └── env-priority/
│ │ │ ├── Dockerfile
│ │ │ ├── compose-with-env-file.yaml
│ │ │ ├── compose-with-env.yaml
│ │ │ └── compose.yaml
│ │ ├── exec/
│ │ │ └── compose.yaml
│ │ ├── export/
│ │ │ └── compose.yaml
│ │ ├── external/
│ │ │ └── compose.yaml
│ │ ├── hooks/
│ │ │ ├── compose.yaml
│ │ │ ├── poststart/
│ │ │ │ ├── compose-error.yaml
│ │ │ │ └── compose-success.yaml
│ │ │ └── prestop/
│ │ │ ├── compose-error.yaml
│ │ │ └── compose-success.yaml
│ │ ├── image-volume-recreate/
│ │ │ ├── Dockerfile
│ │ │ └── compose.yaml
│ │ ├── init-container/
│ │ │ └── compose.yaml
│ │ ├── ipam/
│ │ │ └── compose.yaml
│ │ ├── ipc-test/
│ │ │ └── compose.yaml
│ │ ├── links/
│ │ │ └── compose.yaml
│ │ ├── logging-driver/
│ │ │ └── compose.yaml
│ │ ├── logs-test/
│ │ │ ├── cat.yaml
│ │ │ ├── compose.yaml
│ │ │ └── restart.yaml
│ │ ├── model/
│ │ │ └── compose.yaml
│ │ ├── nested/
│ │ │ └── compose.yaml
│ │ ├── network-alias/
│ │ │ └── compose.yaml
│ │ ├── network-interface-name/
│ │ │ └── compose.yaml
│ │ ├── network-links/
│ │ │ └── compose.yaml
│ │ ├── network-recreate/
│ │ │ └── compose.yaml
│ │ ├── network-test/
│ │ │ ├── compose.subnet.yaml
│ │ │ ├── compose.yaml
│ │ │ └── mac_address.yaml
│ │ ├── no-deps/
│ │ │ ├── network-mode.yaml
│ │ │ └── volume-from.yaml
│ │ ├── orphans/
│ │ │ └── compose.yaml
│ │ ├── pause/
│ │ │ └── compose.yaml
│ │ ├── port-range/
│ │ │ └── compose.yaml
│ │ ├── profiles/
│ │ │ ├── compose.yaml
│ │ │ ├── docker-compose.yaml
│ │ │ └── test-profile.env
│ │ ├── project-volume-bind-test/
│ │ │ └── docker-compose.yml
│ │ ├── providers/
│ │ │ └── depends-on-multiple-providers.yaml
│ │ ├── ps-test/
│ │ │ └── compose.yaml
│ │ ├── publish/
│ │ │ ├── Dockerfile
│ │ │ ├── common.yaml
│ │ │ ├── compose-bind-mount.yml
│ │ │ ├── compose-build-only.yml
│ │ │ ├── compose-env-file.yml
│ │ │ ├── compose-environment.yml
│ │ │ ├── compose-local-include.yml
│ │ │ ├── compose-multi-env-config.yml
│ │ │ ├── compose-sensitive.yml
│ │ │ ├── compose-with-extends.yml
│ │ │ ├── config.txt
│ │ │ ├── oci/
│ │ │ │ ├── compose-override.yaml
│ │ │ │ ├── compose.yaml
│ │ │ │ ├── extends.yaml
│ │ │ │ └── test.env
│ │ │ ├── publish-sensitive.env
│ │ │ ├── publish.env
│ │ │ └── secret.txt
│ │ ├── recreate-volumes/
│ │ │ ├── bind.yaml
│ │ │ ├── compose.yaml
│ │ │ └── compose2.yaml
│ │ ├── resources/
│ │ │ └── compose.yaml
│ │ ├── restart-test/
│ │ │ ├── compose-depends-on.yaml
│ │ │ └── compose.yaml
│ │ ├── run-test/
│ │ │ ├── build-once-nested.yaml
│ │ │ ├── build-once-no-deps.yaml
│ │ │ ├── build-once.yaml
│ │ │ ├── compose.yaml
│ │ │ ├── deps.yaml
│ │ │ ├── orphan.yaml
│ │ │ ├── piped-test.yaml
│ │ │ ├── ports.yaml
│ │ │ ├── pull.yaml
│ │ │ ├── quiet-pull.yaml
│ │ │ └── run.env
│ │ ├── scale/
│ │ │ ├── Dockerfile
│ │ │ ├── build.yaml
│ │ │ └── compose.yaml
│ │ ├── sentences/
│ │ │ └── compose.yaml
│ │ ├── simple-build-test/
│ │ │ ├── compose-interpolate.yaml
│ │ │ ├── compose.yaml
│ │ │ └── nginx-build/
│ │ │ ├── Dockerfile
│ │ │ └── static/
│ │ │ └── index.html
│ │ ├── simple-composefile/
│ │ │ ├── compose.yaml
│ │ │ └── id.yaml
│ │ ├── start-fail/
│ │ │ ├── compose.yaml
│ │ │ └── start-depends_on-long-lived.yaml
│ │ ├── start-stop/
│ │ │ ├── compose.yaml
│ │ │ ├── other.yaml
│ │ │ └── start-stop-deps.yaml
│ │ ├── start_interval/
│ │ │ └── compose.yaml
│ │ ├── stdout-stderr/
│ │ │ ├── compose.yaml
│ │ │ └── log_to_stderr.sh
│ │ ├── stop/
│ │ │ └── compose.yaml
│ │ ├── switch-volumes/
│ │ │ ├── compose.yaml
│ │ │ └── compose2.yaml
│ │ ├── ups-deps-stop/
│ │ │ ├── compose.yaml
│ │ │ └── orphan.yaml
│ │ ├── volume-test/
│ │ │ ├── compose.yaml
│ │ │ ├── nginx-build/
│ │ │ │ └── Dockerfile
│ │ │ └── static/
│ │ │ └── index.html
│ │ ├── volumes/
│ │ │ └── compose.yaml
│ │ ├── wait/
│ │ │ └── compose.yaml
│ │ ├── watch/
│ │ │ ├── compose.yaml
│ │ │ ├── config/
│ │ │ │ └── file.config
│ │ │ ├── data/
│ │ │ │ └── hello.txt
│ │ │ ├── data-logs/
│ │ │ │ └── server.log
│ │ │ ├── exec.yaml
│ │ │ ├── include.yaml
│ │ │ ├── rebuild.yaml
│ │ │ ├── with-external-network.yaml
│ │ │ └── x-initialSync.yaml
│ │ └── wrong-composefile/
│ │ ├── build-error.yml
│ │ ├── compose.yaml
│ │ ├── service1/
│ │ │ └── Dockerfile
│ │ └── unknown-image.yml
│ ├── framework.go
│ ├── healthcheck_test.go
│ ├── hooks_test.go
│ ├── ipc_test.go
│ ├── logs_test.go
│ ├── main_test.go
│ ├── model_test.go
│ ├── networks_test.go
│ ├── noDeps_test.go
│ ├── orphans_test.go
│ ├── pause_test.go
│ ├── profiles_test.go
│ ├── providers_test.go
│ ├── ps_test.go
│ ├── publish_test.go
│ ├── pull_test.go
│ ├── recreate_no_deps_test.go
│ ├── restart_test.go
│ ├── scale_test.go
│ ├── secrets_test.go
│ ├── start_stop_test.go
│ ├── up_test.go
│ ├── volumes_test.go
│ ├── wait_test.go
│ └── watch_test.go
├── mocks/
│ ├── mock_docker_api.go
│ ├── mock_docker_cli.go
│ └── mock_docker_compose_api.go
├── remote/
│ ├── cache.go
│ ├── cache_darwin.go
│ ├── cache_unix.go
│ ├── cache_windows.go
│ ├── git.go
│ ├── git_test.go
│ ├── oci.go
│ └── oci_test.go
├── utils/
│ ├── durationutils.go
│ ├── safebuffer.go
│ ├── set.go
│ ├── set_test.go
│ ├── stringutils.go
│ ├── writer.go
│ └── writer_test.go
└── watch/
├── debounce.go
├── debounce_test.go
├── dockerignore.go
├── dockerignore_test.go
├── ephemeral.go
├── ephemeral_test.go
├── notify.go
├── notify_test.go
├── paths.go
├── paths_test.go
├── temp.go
├── temp_dir_fixture.go
├── watcher_darwin.go
├── watcher_darwin_test.go
├── watcher_naive.go
├── watcher_naive_test.go
├── watcher_nonwin.go
└── watcher_windows.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
bin/
================================================
FILE: .gitattributes
================================================
core.autocrlf false
*.golden text eol=lf
================================================
FILE: .github/CODEOWNERS
================================================
# global rules
* @docker/compose-maintainers
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐞 Bug
description: File a bug/issue
title: "[BUG]
"
labels: ['status/0-triage', 'kind/bug']
body:
- type: textarea
attributes:
label: Description
description: |
Briefly describe the problem you are having.
Include both the current behavior (what you are seeing) as well as what you expected to happen.
validations:
required: true
- type: markdown
attributes:
value: |
[Docker Swarm](https://www.mirantis.com/software/swarm/) uses a distinct compose file parser and
as such doesn't support some of the recent features of Docker Compose. Please contact Mirantis
if you need assistance with compose file support in Docker Swarm.
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Compose Version
description: |
Paste output of `docker compose version` and `docker-compose version`.
render: Text
validations:
required: false
- type: textarea
attributes:
label: Docker Environment
description: Paste output of `docker info`.
render: Text
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Docker Community Slack
url: https://dockr.ly/slack
about: 'Use the #docker-compose channel'
- name: Docker Support Forums
url: https://forums.docker.com/c/open-source-projects/compose/15
about: 'Use the "Open Source Projects > Compose" category'
- name: 'Ask on Stack Overflow'
url: https://stackoverflow.com/questions/tagged/docker-compose
about: 'Use the [docker-compose] tag when creating new questions'
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Feature request
description: Missing functionality? Come tell us about it!
labels:
- kind/feature
- status/0-triage
body:
- type: textarea
id: description
attributes:
label: Description
description: What is the feature you want to see?
validations:
required: true
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
**What I did**
**Related issue**
**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
================================================
FILE: .github/SECURITY.md
================================================
# 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.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: daily
ignore:
# docker + moby deps require coordination
- dependency-name: "github.com/docker/buildx"
# buildx is still 0.x
update-types: ["version-update:semver-minor"]
- dependency-name: "github.com/moby/buildkit"
# buildkit is still 0.x
update-types: [ "version-update:semver-minor" ]
- dependency-name: "github.com/docker/cli"
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/docker/docker"
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/containerd/containerd"
# containerd major/minor must be kept in sync with moby
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
# OTEL dependencies should be upgraded in sync with engine, cli, buildkit and buildx projects
- dependency-name: "go.opentelemetry.io/*"
================================================
FILE: .github/stale.yml
================================================
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- "kind/feature"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
unmarkComment: >
This issue has been automatically marked as not stale anymore due to the recent activity.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed because it had not recent activity during the stale period.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
workflow_dispatch:
inputs:
debug_enabled:
description: 'To run with tmate enter "debug_enabled"'
required: false
default: "false"
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
validate:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- lint
- validate-go-mod
- validate-headers
- validate-docs
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Run
run: |
make ${{ matrix.target }}
binary:
uses: docker/github-builder/.github/workflows/bake.yml@v1.4.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) with GitHub OIDC Token
with:
runner: amd64
artifact-name: compose
artifact-upload: true
cache: true
cache-scope: binary
target: release
output: local
sbom: true
sign: ${{ github.event_name != 'pull_request' }}
binary-finalize:
runs-on: ubuntu-latest
needs:
- binary
steps:
-
name: Download artifacts
uses: actions/download-artifact@v7
with:
path: /tmp/compose-output
name: ${{ needs.binary.outputs.artifact-name }}
-
name: Rename provenance and sbom
run: |
for pdir in /tmp/compose-output/*/; do
(
cd "$pdir"
binname=$(find . -name 'docker-compose-*')
filename=$(basename "${binname%.exe}")
mv "provenance.json" "${filename}.provenance.json"
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
find . -name 'sbom*.json' -exec rm {} \;
if [ -f "provenance.sigstore.json" ]; then
mv "provenance.sigstore.json" "${filename}.sigstore.json"
fi
)
done
mkdir -p "./bin/release"
mv /tmp/compose-output/**/* "./bin/release/"
-
name: Create checksum file
working-directory: ./bin/release
run: |
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
mv $RUNNER_TEMP/checksums.txt .
cat checksums.txt | while read sum file; do
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json && "${file#\*}" != *.sigstore.json ]]; then
echo "$sum $file" > ${file#\*}.sha256
fi
done
-
name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: release
path: ./bin/release/*
if-no-files-found: error
bin-image-test:
if: github.event_name == 'pull_request'
uses: docker/github-builder/.github/workflows/bake.yml@v1.4.0
with:
runner: amd64
target: image-cross
cache: true
cache-scope: bin-image-test
output: image
push: false
sbom: true
set-meta-labels: true
meta-images: |
compose-bin
meta-tags: |
type=ref,event=pr
meta-bake-target: meta-helper
test:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Test
uses: docker/bake-action@v6
with:
targets: test
set: |
*.cache-from=type=gha,scope=test
*.cache-to=type=gha,scope=test
-
name: Gather coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-unit
path: bin/coverage/unit/
if-no-files-found: error
-
name: Unit Test Summary
uses: test-summary/action@v2
with:
paths: bin/coverage/unit/report.xml
if: always()
e2e:
runs-on: ubuntu-latest
name: e2e (${{ matrix.mode }}, ${{ matrix.channel }})
strategy:
fail-fast: false
matrix:
include:
# current stable
- mode: plugin
engine: 29
channel: stable
- mode: standalone
engine: 29
channel: stable
# old stable (latest major - 1)
- mode: plugin
engine: 28
channel: oldstable
- mode: standalone
engine: 28
channel: oldstable
steps:
- name: Prepare
run: |
mode=${{ matrix.mode }}
engine=${{ matrix.engine }}
echo "MODE_ENGINE_PAIR=${mode}-${engine}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker ${{ matrix.engine }}
run: |
sudo systemctl stop docker.service
sudo apt-get purge docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin
sudo apt-get install curl
curl -fsSL https://test.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --version ${{ matrix.engine }}
- name: Check Docker Version
run: docker --version
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Model
run: |
sudo apt-get install docker-model-plugin
docker model version
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: '.go-version'
check-latest: true
cache: true
- name: Build example provider
run: make example-provider
- name: Build
uses: docker/bake-action@v6
with:
source: .
targets: binary-with-coverage
set: |
*.cache-from=type=gha,scope=binary-linux-amd64
*.cache-from=type=gha,scope=binary-e2e-${{ matrix.mode }}
*.cache-to=type=gha,scope=binary-e2e-${{ matrix.mode }},mode=max
env:
BUILD_TAGS: e2e
- name: Setup tmate session
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
with:
limit-access-to-actor: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }}
run: |
rm -rf ./bin/coverage/e2e
mkdir -p ./bin/coverage/e2e
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
- name: Gather coverage data
if: ${{ matrix.mode == 'plugin' }}
uses: actions/upload-artifact@v4
with:
name: coverage-data-e2e-${{ env.MODE_ENGINE_PAIR }}
path: bin/coverage/e2e/
if-no-files-found: error
- name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }}
run: |
rm -f /usr/local/bin/docker-compose
cp bin/build/docker-compose /usr/local/bin
make e2e-compose-standalone
- name: e2e Test Summary
uses: test-summary/action@v2
with:
paths: /tmp/report/report.xml
if: always()
coverage:
runs-on: ubuntu-latest
needs:
- test
- e2e
steps:
# codecov won't process the report without the source code available
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: '.go-version'
check-latest: true
- name: Download unit test coverage
uses: actions/download-artifact@v4
with:
name: coverage-data-unit
path: coverage/unit
merge-multiple: true
- name: Download E2E test coverage
uses: actions/download-artifact@v4
with:
pattern: coverage-data-e2e-*
path: coverage/e2e
merge-multiple: true
- name: Merge coverage reports
run: |
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
- name: Store coverage report in GitHub Actions
uses: actions/upload-artifact@v4
with:
name: go-covdata-txt
path: ./coverage.txt
if-no-files-found: error
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
release:
permissions:
contents: write # to create a release (ncipollo/release-action)
runs-on: ubuntu-latest
needs:
- binary-finalize
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Download artifacts
uses: actions/download-artifact@v7
with:
path: ./bin/release
name: release
-
name: List artifacts
run: |
tree -nh ./bin/release
-
name: Check artifacts
run: |
find bin/release -type f -exec file -e ascii -- {} +
-
name: GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
with:
artifacts: ./bin/release/*
generateReleaseNotes: true
draft: true
token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/docs-upstream.yml
================================================
# this workflow runs the remote validate bake target from docker/docs
# to check if yaml reference docs used in this repo are valid
name: docs-upstream
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
- 'v[0-9]*'
paths:
- '.github/workflows/docs-upstream.yml'
- 'docs/**'
pull_request:
paths:
- '.github/workflows/docs-upstream.yml'
- 'docs/**'
jobs:
docs-yaml:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Upload reference YAML docs
uses: actions/upload-artifact@v4
with:
name: docs-yaml
path: docs/reference
retention-days: 1
validate:
uses: docker/docs/.github/workflows/validate-upstream.yml@main
needs:
- docs-yaml
with:
module-name: docker/compose
================================================
FILE: .github/workflows/merge.yml
================================================
name: merge
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
tags:
- 'v*'
permissions:
contents: read # to fetch code (actions/checkout)
env:
REPO_SLUG: "docker/compose-bin"
jobs:
e2e:
name: Build and test
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [desktop-windows, desktop-macos, desktop-m1]
# mode: [plugin, standalone]
mode: [plugin]
env:
GO111MODULE: "on"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: '.go-version'
cache: true
check-latest: true
- name: List Docker resources on machine
run: |
docker ps --all
docker volume ls
docker network ls
docker image ls
- name: Remove Docker resources on machine
continue-on-error: true
run: |
docker kill $(docker ps -q)
docker rm -f $(docker ps -aq)
docker volume rm -f $(docker volume ls -q)
docker ps --all
- name: Unit tests
run: make test
- name: Build binaries
run: |
make
- name: Check arch of go compose binary
run: |
file ./bin/build/docker-compose
if: ${{ !contains(matrix.os, 'desktop-windows') }}
-
name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }}
run: |
make e2e-compose
-
name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }}
run: |
make e2e-compose-standalone
bin-image-prepare:
runs-on: ubuntu-24.04
outputs:
repo-slug: ${{ env.REPO_SLUG }}
steps:
# FIXME: can't use env object in reusable workflow inputs: https://github.com/orgs/community/discussions/26671
- run: echo "Exposing env vars for reusable workflow"
bin-image:
uses: docker/github-builder/.github/workflows/bake.yml@v1.4.0
needs:
- bin-image-prepare
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) with GitHub OIDC Token
with:
runner: amd64
target: image-cross
cache: true
cache-scope: bin-image
output: image
push: ${{ github.event_name != 'pull_request' }}
sbom: true
set-meta-labels: true
meta-images: |
${{ needs.bin-image-prepare.outputs.repo-slug }}
meta-tags: |
type=ref,event=tag
type=edge
meta-bake-target: meta-helper
secrets:
registry-auths: |
- registry: docker.io
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
desktop-edge-test:
runs-on: ubuntu-latest
needs: bin-image
steps:
-
name: Generate Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.DOCKERDESKTOP_APP_ID }}
private-key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
owner: docker
repositories: |
${{ secrets.DOCKERDESKTOP_REPO }}
-
name: Trigger Docker Desktop e2e with edge version
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'docker',
repo: '${{ secrets.DOCKERDESKTOP_REPO }}',
workflow_id: 'compose-edge-integration.yml',
ref: 'main',
inputs: {
"image-tag": "${{ env.REPO_SLUG }}:edge"
}
})
================================================
FILE: .github/workflows/scorecards.yml
================================================
name: Scorecards supply-chain security
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '44 9 * * 4'
push:
branches: [ "main" ]
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Used to receive a badge.
id-token: write
# read permissions to all the other objects
actions: read
attestations: read
checks: read
contents: read
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: read
statuses: read
steps:
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.4.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # tag=v2.4.0
with:
results_file: results.sarif
results_format: sarif
# Publish the results for public repositories to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # tag=v4.5.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@3096afedf9873361b2b2f65e1445b13272c83eb8 # tag=v2.20.00
with:
sarif_file: results.sarif
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale issues'
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
schedule:
- cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
days-before-issue-stale: 150 # marks stale after 5 months
days-before-issue-close: 30 # closes 1 month after being marked with no action
stale-issue-label: "stale"
exempt-issue-labels: "kind/feature,kind/enhancement"
================================================
FILE: .gitignore
================================================
bin/
/.vscode/
coverage.out
covdatafiles/
.DS_Store
pkg/e2e/*.tar
================================================
FILE: .go-version
================================================
1.25.8
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
concurrency: 2
linters:
default: none
enable:
- copyloopvar
- depguard
- errcheck
- errorlint
- forbidigo
- gocritic
- gocyclo
- gomodguard
- govet
- ineffassign
- lll
- misspell
- nakedret
- nolintlint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
settings:
depguard:
rules:
all:
deny:
- pkg: io/ioutil
desc: io/ioutil package has been deprecated
- pkg: github.com/docker/docker/errdefs
desc: use github.com/containerd/errdefs instead.
- pkg: golang.org/x/exp/maps
desc: use stdlib maps package
- pkg: golang.org/x/exp/slices
desc: use stdlib slices package
- pkg: gopkg.in/yaml.v2
desc: compose-go uses yaml.v3
forbidigo:
analyze-types: true
forbid:
- pattern: 'context\.Background'
pkg: '^context$'
msg: "in tests, use t.Context() instead of context.Background()"
- pattern: 'context\.TODO'
pkg: '^context$'
msg: "in tests, use t.Context() instead of context.TODO()"
gocritic:
disabled-checks:
- paramTypeCombine
- unnamedResult
- whyNoLint
enabled-tags:
- diagnostic
- opinionated
- style
gocyclo:
min-complexity: 16
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
- fmt
versions:
- github.com/distribution/distribution:
reason: use distribution/reference
- gotest.tools:
version: < 3.0.0
reason: deprecated, pre-modules version
lll:
line-length: 200
revive:
rules:
- name: package-comments
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
rules:
- path-except: '_test\.go'
linters:
- forbidigo
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gci
- gofumpt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
settings:
gci:
sections:
- standard
- default
- localmodule
custom-order: true # make the section order the same as the order of "sections".
================================================
FILE: BUILDING.md
================================================
### Prerequisites
* Windows:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* macOS:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* Linux:
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
* make
* go (see [go.mod](go.mod) for minimum version)
### Building the CLI
Once you have the prerequisites installed, you can build the CLI using:
```console
make
```
This will output a `docker-compose` CLI plugin for your host machine in
`./bin/build`.
You can statically cross compile the CLI for Windows, macOS, and Linux using the
`cross` target.
### Unit tests
To run all of the unit tests, run:
```console
make test
```
If you need to update a golden file simply do `go test ./... -test.update-golden`.
### End-to-end tests
To run e2e tests, the Compose CLI binary needs to be built. All the commands to run e2e tests propose a version
with the prefix `build-and-e2e` to first build the CLI before executing tests.
Note that this requires a local Docker Engine to be running.
#### Whole end-to-end tests suite
To execute both CLI and standalone e2e tests, run :
```console
make e2e
```
Or if you need to build the CLI, run:
```console
make build-and-e2e
```
#### Plugin end-to-end tests suite
To execute CLI plugin e2e tests, run :
```console
make e2e-compose
```
Or if you need to build the CLI, run:
```console
make build-and-e2e-compose
```
#### Standalone end-to-end tests suite
To execute the standalone CLI e2e tests, run :
```console
make e2e-compose-standalone
```
Or if you need to build the CLI, run:
```console
make build-and-e2e-compose-standalone
```
## Releases
To create a new release:
* Check that the CI is green on the main branch for the commit you want to release
* Run the release GitHub Actions workflow with a tag of form vx.y.z following existing tags.
This will automatically create a new tag, release and make binaries for
Windows, macOS, and Linux available for download on the
[releases page](https://github.com/docker/compose/releases).
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/contribute/).
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
- [Contributing to Docker](#contributing-to-docker)
- [Topics](#topics)
- [Reporting security issues](#reporting-security-issues)
- [Reporting other issues](#reporting-other-issues)
- [Quick contribution tips and guidelines](#quick-contribution-tips-and-guidelines)
- [Pull requests are always welcome](#pull-requests-are-always-welcome)
- [Talking to other Docker users and contributors](#talking-to-other-docker-users-and-contributors)
- [Conventions](#conventions)
- [Merge approval](#merge-approval)
- [Sign your work](#sign-your-work)
- [How can I become a maintainer?](#how-can-i-become-a-maintainer)
- [Docker community guidelines](#docker-community-guidelines)
- [Coding Style](#coding-style)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead, send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for them.
We also like to send gifts—if you're into Docker swag, make sure to let
us know. We currently do not offer a paid security bounty program but are not
ruling it out in the future.
## Reporting other issues
A great way to contribute to the project is to send a detailed report when you
encounter an issue. We always appreciate a well-written, thorough bug report,
and will thank you for it!
Check that [our issue database](https://github.com/docker/compose/labels/Docker%20Compose%20V2)
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, you can use the "subscribe" button to get notified of
updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help to resolve it. However, if you
have ways to reproduce the issue or have additional information that may help
resolve the issue, please leave a comment.
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker context show`.
* The output of `docker info`.
Also, include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log files, consider posting them as a gist
(https://gist.github.com).
Don't forget to remove sensitive data from your log files before posting (you
can replace those parts with "REDACTED").
_Note:_
Maintainers might request additional information to diagnose an issue,
if initial reporter doesn't answer within a reasonable delay (a few weeks),
issue will be closed.
## Quick contribution tips and guidelines
This section gives the experienced contributor some tips and guidelines.
### Pull requests are always welcome
Not sure if that typo is worth a pull request? Found a bug and know how to fix
it? Do it! We will appreciate it. Any significant change, like adding a backend,
should be documented as
[a GitHub issue](https://github.com/docker/compose/issues)
before anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try,
don't get discouraged!
### Talking to other Docker users and contributors
| Community Slack |
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up with this link.
|
| Forums |
A public forum for users to discuss questions and explore current design patterns and
best practices about Docker and related projects in the Docker Ecosystem. To participate,
just log in with your Docker Hub account on https://forums.docker.com.
|
| Twitter |
You can follow Docker's Twitter feed
to get updates on our products. You can also tweet us questions or just
share blogs or stories.
|
| Stack Overflow |
Stack Overflow has over 17000 Docker questions listed. We regularly
monitor Docker questions
and so do many other knowledgeable Docker users.
|
### Conventions
Fork the repository and make changes on your fork in a feature branch:
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
the issue.
- If it's a feature branch, create an enhancement issue to announce
your intentions, and name it XXXX-something where XXXX is the number of the
issue.
Submit unit tests for your changes. Go has a great test framework built in; use
it! Take a look at existing tests for inspiration. Also, end-to-end tests are
available. Run the full test suite, both unit tests and e2e tests on your
branch before submitting a pull request. See [BUILDING.md](BUILDING.md) for
instructions to build and run tests.
Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address.
Commit messages must start with a capitalized and short summary (max. 50 chars)
written in the imperative, followed by an optional, more detailed explanatory
text which is separated from the summary by an empty line.
Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post
a comment after pushing. New commits show up in the pull request automatically,
but the reviewers are notified only when you comment.
Pull requests must be cleanly rebased on top of the base branch without multiple branches
mixed into the PR.
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
feature branch to update your pull request rather than `merge master`.
Before you make a pull request, squash your commits into logical units of work
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
set of patches that should be reviewed together: for example, upgrading the
version of a vendored dependency and taking advantage of its now available new
feature constitute two separate units of work. Implementing a new function and
calling it in another file constitute a single logical unit of work. The very
high majority of submissions should have a single commit, so if in doubt: squash
down to one.
After every commit, make sure the test suite passes. Include documentation
changes in the same pull request so that a revert would remove all traces of
the feature or fix.
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull
request description that closes an issue. Including references automatically
closes the issue on a merge.
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history.
Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
indicate acceptance.
A change requires at least 2 LGTMs from the maintainers of each
component affected.
For more details, see the [MAINTAINERS](MAINTAINERS) page.
### Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](https://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
### How can I become a maintainer?
The procedures for adding new maintainers are explained in the global
[MAINTAINERS](https://github.com/docker/opensource/blob/main/MAINTAINERS)
file in the
[https://github.com/docker/opensource/](https://github.com/docker/opensource/)
repository.
Don't forget: being a maintainer is a time investment. Make sure you
will have time to make yourself available. You don't have to be a
maintainer to make a difference on the project!
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members:
no regional, racial, gender or other abuse will be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending it to a large number of people. Please
consider this before you update. Also, remember that nobody likes spam.
* Don't send emails to the maintainers: There's no need to send emails to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
## Coding Style
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributors should make their
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](https://go.dev/doc/effective_go) and [Go Code Review
Comments](https://go.dev/wiki/CodeReviewComments).
4. Include code comments. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://go.dev/doc/effective_go). The
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.
================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.25.8
ARG XX_VERSION=1.9.0
ARG GOLANGCI_LINT_VERSION=v2.8.0
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"
ARG DOCS_FORMATS="md,yaml"
ARG LICENSE_FILES=".*\(Dockerfile\|Makefile\|\.go\|\.hcl\|\.sh\)"
# xx is a helper for cross-compilation
FROM --platform=${BUILDPLATFORM} tonistiigi/xx:${XX_VERSION} AS xx
# osxcross contains the MacOSX cross toolchain for xx
FROM crazymax/osxcross:15.5-alpine AS osxcross
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
FROM ghcr.io/google/addlicense:${ADDLICENSE_VERSION} AS addlicense
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine3.22 AS base
COPY --from=xx / /
RUN apk add --no-cache \
clang \
docker \
file \
findutils \
git \
make \
protoc \
protobuf-dev
WORKDIR /src
ENV CGO_ENABLED=0
FROM base AS build-base
COPY go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
FROM build-base AS vendored
RUN --mount=type=bind,target=.,rw \
--mount=type=cache,target=/go/pkg/mod \
go mod tidy && mkdir /out && cp go.mod go.sum /out
FROM scratch AS vendor-update
COPY --from=vendored /out /
FROM vendored AS vendor-validate
RUN --mount=type=bind,target=.,rw <&2 'ERROR: Vendor result differs. Please vendor your package with "make go-mod-tidy"'
echo "$diff"
exit 1
fi
EOT
FROM build-base AS build
ARG BUILD_TAGS
ARG BUILD_FLAGS
ARG TARGETPLATFORM
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
xx-go --wrap && \
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; export BUILD_TAGS=fsnotify,$BUILD_TAGS; fi && \
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
xx-verify --static /out/docker-compose
FROM build-base AS lint
ARG BUILD_TAGS
ENV GOLANGCI_LINT_CACHE=/cache/golangci-lint
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/cache/golangci-lint \
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
golangci-lint cache status && \
golangci-lint run --build-tags "$BUILD_TAGS" ./...
FROM build-base AS test
ARG CGO_ENABLED=0
ARG BUILD_TAGS
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
rm -rf /tmp/coverage && \
mkdir -p /tmp/coverage && \
rm -rf /tmp/report && \
mkdir -p /tmp/report && \
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
go tool covdata percent -i=/tmp/coverage
FROM scratch AS test-coverage
COPY --from=test --link /tmp/coverage /
COPY --from=test --link /tmp/report /
FROM base AS license-set
ARG LICENSE_FILES
RUN --mount=type=bind,target=.,rw \
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
find . -regex "${LICENSE_FILES}" | xargs addlicense -c 'Docker Compose CLI' -l apache && \
mkdir /out && \
find . -regex "${LICENSE_FILES}" | cpio -pdm /out
FROM scratch AS license-update
COPY --from=set /out /
FROM base AS license-validate
ARG LICENSE_FILES
RUN --mount=type=bind,target=. \
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
find . -regex "${LICENSE_FILES}" | xargs addlicense -check -c 'Docker Compose CLI' -l apache -ignore validate -ignore testdata -ignore resolvepath -v
FROM base AS docsgen
WORKDIR /src
RUN --mount=target=. \
--mount=target=/root/.cache,type=cache \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/docsgen ./docs/yaml/main/generate.go
FROM --platform=${BUILDPLATFORM} alpine AS docs-build
RUN apk add --no-cache rsync git
WORKDIR /src
COPY --from=docsgen /out/docsgen /usr/bin
ARG DOCS_FORMATS
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <&2 'ERROR: Docs result differs. Please update with "make docs"'
git status --porcelain -- docs/reference
exit 1
fi
EOT
FROM scratch AS binary-unix
COPY --link --from=build /out/docker-compose /
FROM binary-unix AS binary-darwin
FROM binary-unix AS binary-linux
FROM scratch AS binary-windows
COPY --link --from=build /out/docker-compose /docker-compose.exe
FROM binary-$TARGETOS AS binary
# enable scanning for this stage
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM --platform=$BUILDPLATFORM alpine AS releaser
WORKDIR /work
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN --mount=from=binary \
mkdir -p /out && \
# TODO: should just use standard arch
TARGETARCH=$([ "$TARGETARCH" = "amd64" ] && echo "x86_64" || echo "$TARGETARCH"); \
TARGETARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "$TARGETARCH"); \
cp docker-compose* "/out/docker-compose-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}$(ls docker-compose* | sed -e 's/^docker-compose//')"
FROM scratch AS release
COPY --from=releaser /out/ /
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
================================================
FILE: Makefile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
PKG := github.com/docker/compose/v5
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
GO_BUILDTAGS ?= e2e
DRIVE_PREFIX?=
ifeq ($(OS),Windows_NT)
DETECTED_OS = Windows
DRIVE_PREFIX=C:
else
DETECTED_OS = $(shell uname -s)
endif
ifeq ($(DETECTED_OS),Windows)
BINARY_EXT=.exe
endif
BUILD_FLAGS?=
TEST_FLAGS?=
E2E_TEST?=
ifneq ($(E2E_TEST),)
TEST_FLAGS:=$(TEST_FLAGS) -run '$(E2E_TEST)'
endif
EXCLUDE_E2E_TESTS?=
ifneq ($(EXCLUDE_E2E_TESTS),)
TEST_FLAGS:=$(TEST_FLAGS) -skip '$(EXCLUDE_E2E_TESTS)'
endif
BUILDX_CMD ?= docker buildx
# DESTDIR overrides the output path for binaries and other artifacts
# this is used by docker/docker-ce-packaging for the apt/rpm builds,
# so it's important that the resulting binary ends up EXACTLY at the
# path $DESTDIR/docker-compose when specified.
#
# See https://github.com/docker/docker-ce-packaging/blob/e43fbd37e48fde49d907b9195f23b13537521b94/rpm/SPECS/docker-compose-plugin.spec#L47
#
# By default, all artifacts go to subdirectories under ./bin/ in the
# repo root, e.g. ./bin/build, ./bin/coverage, ./bin/release.
DESTDIR ?=
all: build
.PHONY: build ## Build the compose cli-plugin
build:
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(or $(DESTDIR),./bin/build)/docker-compose$(BINARY_EXT)" ./cmd
.PHONY: binary
binary:
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary
.PHONY: binary-with-coverage
binary-with-coverage:
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary-with-coverage
.PHONY: install
install: binary
mkdir -p ~/.docker/cli-plugins
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
.PHONY: e2e-compose
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e
.PHONY: e2e-compose-standalone
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e
.PHONY: build-and-e2e-compose
build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
.PHONY: build-and-e2e-compose-standalone
build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
.PHONY: example-provider
example-provider: ## build example provider for e2e tests
go build -o bin/build/example-provider docs/examples/provider.go
.PHONY: mocks
mocks:
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0
mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/moby/moby/client APIClient
mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
.PHONY: e2e
e2e: e2e-compose e2e-compose-standalone ## Run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
.PHONY: build-and-e2e
build-and-e2e: build e2e-compose e2e-compose-standalone ## Compile the compose cli-plugin and run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
.PHONY: cross
cross: ## Compile the CLI for linux, darwin and windows
$(BUILDX_CMD) bake binary-cross
.PHONY: test
test: ## Run unit tests
$(BUILDX_CMD) bake test
.PHONY: cache-clear
cache-clear: ## Clear the builder cache
$(BUILDX_CMD) prune --force --filter type=exec.cachemount --filter=unused-for=24h
.PHONY: lint
lint: ## run linter(s)
$(BUILDX_CMD) bake lint
.PHONY: fmt
fmt:
gofumpt --version >/dev/null 2>&1 || go install mvdan.cc/gofumpt@latest
gofumpt -w .
.PHONY: docs
docs: ## generate documentation
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))
$(BUILDX_CMD) bake --set "*.output=type=local,dest=$($@_TMP_OUT)" docs-update
rm -rf ./docs/internal
cp -R "$(DRIVE_PREFIX)$($@_TMP_OUT)"/out/* ./docs/
rm -rf "$(DRIVE_PREFIX)$($@_TMP_OUT)"/*
.PHONY: validate-docs
validate-docs: ## validate the doc does not change
$(BUILDX_CMD) bake docs-validate
.PHONY: check-dependencies
check-dependencies: ## check dependency updates
go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all
.PHONY: validate-headers
validate-headers: ## Check license header for all files
$(BUILDX_CMD) bake license-validate
.PHONY: go-mod-tidy
go-mod-tidy: ## Run go mod tidy in a container and output resulting go.mod and go.sum
$(BUILDX_CMD) bake vendor-update
.PHONY: validate-go-mod
validate-go-mod: ## Validate go.mod and go.sum are up-to-date
$(BUILDX_CMD) bake vendor-validate
validate: validate-go-mod validate-headers validate-docs ## Validate sources
pre-commit: validate check-dependencies lint build test e2e-compose
help: ## Show help
@echo Please specify a build target. The choices are:
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
================================================
FILE: NOTICE
================================================
Docker Compose V2
Copyright 2020 Docker Compose authors
This product includes software developed at Docker, Inc. (https://www.docker.com).
================================================
FILE: README.md
================================================
# Table of Contents
- [Docker Compose](#docker-compose)
- [Where to get Docker Compose](#where-to-get-docker-compose)
+ [Windows and macOS](#windows-and-macos)
+ [Linux](#linux)
- [Quick Start](#quick-start)
- [Contributing](#contributing)
- [Legacy](#legacy)
# Docker Compose
[](https://github.com/docker/compose/releases/latest)
[](https://pkg.go.dev/github.com/docker/compose/v5)
[](https://github.com/docker/compose/actions?query=workflow%3Aci)
[](https://goreportcard.com/report/github.com/docker/compose/v5)
[](https://codecov.io/gh/docker/compose)
[](https://api.securityscorecards.dev/projects/github.com/docker/compose)

Docker Compose is a tool for running multi-container applications on Docker
defined using the [Compose file format](https://compose-spec.io).
A Compose file is used to define how one or more containers that make up
your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: `docker compose up`.
> **Note**: About Docker Swarm
> Docker Swarm used to rely on the legacy compose file format but did not adopt the compose specification
> so is missing some of the recent enhancements in the compose syntax. After
> [acquisition by Mirantis](https://www.mirantis.com/software/swarm/) swarm isn't maintained by Docker Inc, and
> as such some Docker Compose features aren't accessible to swarm users.
# Where to get Docker Compose
### Windows and macOS
Docker Compose is included in
[Docker Desktop](https://www.docker.com/products/docker-desktop/)
for Windows and macOS.
### Linux
You can download Docker Compose binaries from the
[release page](https://github.com/docker/compose/releases) on this repository.
Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins`
Or copy it into one of these folders to install it system-wide:
* `/usr/local/lib/docker/cli-plugins` OR `/usr/local/libexec/docker/cli-plugins`
* `/usr/lib/docker/cli-plugins` OR `/usr/libexec/docker/cli-plugins`
(might require making the downloaded file executable with `chmod +x`)
Quick Start
-----------
Using Docker Compose is a three-step process:
1. Define your app's environment with a `Dockerfile` so it can be
reproduced anywhere.
2. Define the services that make up your app in `compose.yaml` so
they can be run together in an isolated environment.
3. Lastly, run `docker compose up` and Compose will start and run your entire
app.
A Compose file looks like this:
```yaml
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
redis:
image: redis
```
Contributing
------------
Want to help develop Docker Compose? Check out our
[contributing documentation](CONTRIBUTING.md).
If you find an issue, please report it on the
[issue tracker](https://github.com/docker/compose/issues/new/choose).
Legacy
-------------
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1).
================================================
FILE: cmd/cmdtrace/cmd_span.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmdtrace
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
commands "github.com/docker/compose/v5/cmd/compose"
"github.com/docker/compose/v5/internal/tracing"
)
// Setup should be called as part of the command's PersistentPreRunE
// as soon as possible after initializing the dockerCli.
//
// It initializes the tracer for the CLI using both auto-detection
// from the Docker context metadata as well as standard OTEL_ env
// vars, creates a root span for the command, and wraps the actual
// command invocation to ensure the span is properly finalized and
// exported before exit.
func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
tracingShutdown, err := tracing.InitTracing(dockerCli)
if err != nil {
return fmt.Errorf("initializing tracing: %w", err)
}
ctx := cmd.Context()
ctx, cmdSpan := otel.Tracer("").Start(
ctx,
"cli/"+strings.Join(commandName(cmd), "-"),
)
cmdSpan.SetAttributes(
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
)
cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown)
return nil
}
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
// method. This is necessary to capture the command result for reporting as well
// as flushing any spans before exit.
//
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
// only runs if RunE does _not_ return an error, but this should run unconditionally.
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
origRunE := c.RunE
if origRunE == nil {
origRun := c.Run
//nolint:unparam // wrapper function for RunE, always returns nil by design
origRunE = func(cmd *cobra.Command, args []string) error {
origRun(cmd, args)
return nil
}
c.Run = nil
}
c.RunE = func(cmd *cobra.Command, args []string) error {
cmdErr := origRunE(cmd, args)
if cmdSpan != nil {
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
// default exit code is 1 if a more descriptive error
// wasn't returned
exitCode := 1
var statusErr dockercli.StatusError
if errors.As(cmdErr, &statusErr) {
exitCode = statusErr.StatusCode
}
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
attribute.Int("exit_code", exitCode),
))
} else {
cmdSpan.SetStatus(codes.Ok, "")
}
cmdSpan.End()
}
if tracingShutdown != nil {
// use background for root context because the cmd's context might have
// been canceled already
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// TODO(milas): add an env var to enable logging from the
// OTel components for debugging purposes
_ = tracingShutdown(ctx)
}
return cmdErr
}
}
// commandName returns the path components for a given command,
// in reverse alphabetical order for consistent usage metrics.
//
// The root Compose command and anything before (i.e. "docker")
// are not included.
//
// For example:
// - docker compose alpha watch -> [watch, alpha]
// - docker-compose up -> [up]
func commandName(cmd *cobra.Command) []string {
var name []string
for c := cmd; c != nil; c = c.Parent() {
if c.Name() == commands.PluginName {
break
}
name = append(name, c.Name())
}
sort.Sort(sort.Reverse(sort.StringSlice(name)))
return name
}
func getFlags(fs *flag.FlagSet) []string {
var result []string
fs.Visit(func(flag *flag.Flag) {
result = append(result, flag.Name)
})
return result
}
================================================
FILE: cmd/cmdtrace/cmd_span_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmdtrace
import (
"reflect"
"testing"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
commands "github.com/docker/compose/v5/cmd/compose"
)
func TestGetFlags(t *testing.T) {
// Initialize flagSet with flags
fs := flag.NewFlagSet("up", flag.ContinueOnError)
var (
detach string
timeout string
)
fs.StringVar(&detach, "detach", "d", "")
fs.StringVar(&timeout, "timeout", "t", "")
_ = fs.Set("detach", "detach")
_ = fs.Set("timeout", "timeout")
tests := []struct {
name string
input *flag.FlagSet
expected []string
}{
{
name: "NoFlags",
input: flag.NewFlagSet("NoFlags", flag.ContinueOnError),
expected: nil,
},
{
name: "Flags",
input: fs,
expected: []string{"detach", "timeout"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := getFlags(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("Expected %v, but got %v", test.expected, result)
}
})
}
}
func TestCommandName(t *testing.T) {
tests := []struct {
name string
setupCmd func() *cobra.Command
want []string
}{
{
name: "docker compose alpha watch -> [watch, alpha]",
setupCmd: func() *cobra.Command {
dockerCmd := &cobra.Command{Use: "docker"}
composeCmd := &cobra.Command{Use: commands.PluginName}
alphaCmd := &cobra.Command{Use: "alpha"}
watchCmd := &cobra.Command{Use: "watch"}
dockerCmd.AddCommand(composeCmd)
composeCmd.AddCommand(alphaCmd)
alphaCmd.AddCommand(watchCmd)
return watchCmd
},
want: []string{"watch", "alpha"},
},
{
name: "docker-compose up -> [up]",
setupCmd: func() *cobra.Command {
dockerComposeCmd := &cobra.Command{Use: commands.PluginName}
upCmd := &cobra.Command{Use: "up"}
dockerComposeCmd.AddCommand(upCmd)
return upCmd
},
want: []string{"up"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.setupCmd()
got := commandName(cmd)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("commandName() = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: cmd/compatibility/convert.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compatibility
import (
"fmt"
"os"
"slices"
"strings"
"github.com/docker/compose/v5/cmd/compose"
)
func getCompletionCommands() []string {
return []string{
"__complete",
"__completeNoDesc",
}
}
func getBoolFlags() []string {
return []string{
"--debug", "-D",
"--verbose",
"--tls",
"--tlsverify",
}
}
func getStringFlags() []string {
return []string{
"--tlscacert",
"--tlscert",
"--tlskey",
"--host", "-H",
"--context",
"--log-level",
}
}
// Convert transforms standalone docker-compose args into CLI plugin compliant ones
func Convert(args []string) []string {
var rootFlags []string
command := []string{compose.PluginName}
l := len(args)
ARGS:
for i := 0; i < l; i++ {
arg := args[i]
if slices.Contains(getCompletionCommands(), arg) {
command = append([]string{arg}, command...)
continue
}
if arg != "" && arg[0] != '-' {
command = append(command, args[i:]...)
break
}
switch arg {
case "--verbose":
arg = "--debug"
case "-h":
// docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it
arg = "--help"
case "--version", "-v":
// redirect --version pseudo-command to actual command
arg = "version"
}
if slices.Contains(getBoolFlags(), arg) {
rootFlags = append(rootFlags, arg)
continue
}
for _, flag := range getStringFlags() {
if arg == flag {
i++
if i >= l {
fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg)
os.Exit(1)
}
rootFlags = append(rootFlags, arg, args[i])
continue ARGS
}
if strings.HasPrefix(arg, flag) {
_, val, found := strings.Cut(arg, "=")
if found {
rootFlags = append(rootFlags, flag, val)
continue ARGS
}
}
}
command = append(command, arg)
}
return append(rootFlags, command...)
}
================================================
FILE: cmd/compatibility/convert_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compatibility
import (
"errors"
"os"
"os/exec"
"testing"
"gotest.tools/v3/assert"
)
func Test_convert(t *testing.T) {
tests := []struct {
name string
args []string
want []string
wantErr bool
}{
{
name: "compose only",
args: []string{"up"},
want: []string{"compose", "up"},
},
{
name: "with context",
args: []string{"--context", "foo", "-f", "compose.yaml", "up"},
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
},
{
name: "with context arg",
args: []string{"--context=foo", "-f", "compose.yaml", "up"},
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
},
{
name: "with host",
args: []string{"--host", "tcp://1.2.3.4", "up"},
want: []string{"--host", "tcp://1.2.3.4", "compose", "up"},
},
{
name: "compose --verbose",
args: []string{"--verbose"},
want: []string{"--debug", "compose"},
},
{
name: "compose --version",
args: []string{"--version"},
want: []string{"compose", "version"},
},
{
name: "compose -v",
args: []string{"-v"},
want: []string{"compose", "version"},
},
{
name: "help",
args: []string{"-h"},
want: []string{"compose", "--help"},
},
{
name: "issues/1962",
args: []string{"psql", "-h", "postgres"},
want: []string{"compose", "psql", "-h", "postgres"}, // -h should not be converted to --help
},
{
name: "issues/8648",
args: []string{"exec", "mongo", "mongo", "--host", "mongo"},
want: []string{"compose", "exec", "mongo", "mongo", "--host", "mongo"}, // --host is passed to exec
},
{
name: "issues/12",
args: []string{"--log-level", "INFO", "up"},
want: []string{"--log-level", "INFO", "compose", "up"},
},
{
name: "empty string argument",
args: []string{"--project-directory", "", "ps"},
want: []string{"compose", "--project-directory", "", "ps"},
},
{
name: "compose as project name",
args: []string{"--project-name", "compose", "down", "--remove-orphans"},
want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"},
},
{
name: "completion command",
args: []string{"__complete", "up"},
want: []string{"__complete", "compose", "up"},
},
{
name: "string flag without argument",
args: []string{"--log-level"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr {
if os.Getenv("BE_CRASHER") == "1" {
Convert(tt.args)
return
}
cmd := exec.Command(os.Args[0], "-test.run=^"+t.Name()+"$")
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
err := cmd.Run()
var e *exec.ExitError
if errors.As(err, &e) && !e.Success() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
} else {
got := Convert(tt.args)
assert.DeepEqual(t, tt.want, got)
}
})
}
}
================================================
FILE: cmd/compose/alpha.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// alphaCommand groups all experimental subcommands
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
cmd := &cobra.Command{
Short: "Experimental commands",
Use: "alpha [COMMAND]",
Hidden: true,
Annotations: map[string]string{
"experimentalCLI": "true",
},
}
cmd.AddCommand(
vizCommand(p, dockerCli, backendOptions),
publishCommand(p, dockerCli, backendOptions),
generateCommand(p, dockerCli, backendOptions),
)
return cmd
}
================================================
FILE: cmd/compose/attach.go
================================================
/*
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/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type attachOpts struct {
*composeOptions
service string
index int
detachKeys string
noStdin bool
proxy bool
}
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := attachOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "attach [OPTIONS] SERVICE",
Short: "Attach local standard input, output, and error streams to a service's running container",
Args: cobra.MinimumNArgs(1),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runAttach(ctx, dockerCli, backendOptions, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas.")
runCmd.Flags().StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching from a container.")
runCmd.Flags().BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
runCmd.Flags().BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
return runCmd
}
func runAttach(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts attachOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
attachOpts := api.AttachOptions{
Service: opts.service,
Index: opts.index,
DetachKeys: opts.detachKeys,
NoStdin: opts.noStdin,
Proxy: opts.proxy,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Attach(ctx, projectName, attachOpts)
}
================================================
FILE: cmd/compose/bridge.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client/pkg/stringid"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/bridge"
"github.com/docker/compose/v5/pkg/compose"
)
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "bridge CMD [OPTIONS]",
Short: "Convert compose files into another model",
TraverseChildren: true,
}
cmd.AddCommand(
convertCommand(p, dockerCli),
transformersCommand(dockerCli),
)
return cmd
}
func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
convertOpts := bridge.ConvertOptions{}
cmd := &cobra.Command{
Use: "convert",
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runConvert(ctx, dockerCli, p, convertOpts)
}),
}
flags := cmd.Flags()
flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
return cmd
}
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := p.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}
return bridge.Convert(ctx, dockerCli, project, opts)
}
func transformersCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "transformations CMD [OPTIONS]",
Short: "Manage transformation images",
}
cmd.AddCommand(
listTransformersCommand(dockerCli),
createTransformerCommand(dockerCli),
)
return cmd
}
func listTransformersCommand(dockerCli command.Cli) *cobra.Command {
options := lsOptions{}
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available transformations",
RunE: Adapt(func(ctx context.Context, args []string) error {
transformers, err := bridge.ListTransformers(ctx, dockerCli)
if err != nil {
return err
}
return displayTransformer(dockerCli, transformers, options)
}),
}
cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]")
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names")
return cmd
}
func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error {
if options.Quiet {
for _, t := range transformers {
if len(t.RepoTags) > 0 {
_, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0])
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), t.ID)
}
}
return nil
}
return formatter.Print(transformers, options.Format, dockerCli.Out(),
func(w io.Writer) {
for _, img := range transformers {
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo, tag := "", ""
if len(img.RepoTags) > 0 {
ref, err := reference.ParseDockerRef(img.RepoTags[0])
if err == nil {
// ParseDockerRef will reject a local image ID
repo = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size)
}
},
"IMAGE ID", "REPO", "TAGS", "SIZE")
}
func createTransformerCommand(dockerCli command.Cli) *cobra.Command {
var opts bridge.CreateTransformerOptions
cmd := &cobra.Command{
Use: "create [OPTION] PATH",
Short: "Create a new transformation",
RunE: Adapt(func(ctx context.Context, args []string) error {
opts.Dest = args[0]
return bridge.CreateTransformer(ctx, dockerCli, opts)
}),
}
cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)")
return cmd
}
================================================
FILE: cmd/compose/build.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"strings"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type buildOptions struct {
*ProjectOptions
quiet bool
pull bool
push bool
args []string
noCache bool
memory cliopts.MemBytes
ssh string
builder string
deps bool
print bool
check bool
sbom string
provenance string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
var SSHKeys []types.SSHKey
if opts.ssh != "" {
id, path, found := strings.Cut(opts.ssh, "=")
if !found && id != "default" {
return api.BuildOptions{}, fmt.Errorf("invalid ssh key %q", opts.ssh)
}
SSHKeys = append(SSHKeys, types.SSHKey{
ID: id,
Path: path,
})
}
builderName := opts.builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
}
uiMode := display.Mode
if uiMode == display.ModeJSON {
uiMode = "rawjson"
}
return api.BuildOptions{
Pull: opts.pull,
Push: opts.push,
Progress: uiMode,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
Deps: opts.deps,
Memory: int64(opts.memory),
Print: opts.print,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
SBOM: opts.sbom,
Provenance: opts.provenance,
}, nil
}
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "build [OPTIONS] [SERVICE...]",
Short: "Build or rebuild services",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
display.Mode = display.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
opts.ssh = "default"
}
if cmd.Flags().Changed("progress") && opts.ssh == "" {
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...\n")
}
return runBuild(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVar(&opts.push, "push", false, "Push service images")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output")
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image")
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services")
flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
flags.StringVar(&opts.builder, "builder", "", "Set builder to use")
flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)")
flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`)
flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`)
flags.Bool("parallel", true, "Build images in parallel. DEPRECATED")
flags.MarkHidden("parallel") //nolint:errcheck
flags.Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
flags.MarkHidden("compress") //nolint:errcheck
flags.Bool("force-rm", true, "Always remove intermediate containers. DEPRECATED")
flags.MarkHidden("force-rm") //nolint:errcheck
flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the image")
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
flags.MarkHidden("no-rm") //nolint:errcheck
flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
flags.StringVar(&p.Progress, "progress", "", fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
flags.MarkHidden("progress") //nolint:errcheck
flags.BoolVar(&opts.print, "print", false, "Print equivalent bake file")
flags.BoolVar(&opts.check, "check", false, "Check build configuration")
return cmd
}
func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
if opts.print {
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if err := applyPlatforms(project, false); err != nil {
return err
}
apiBuildOptions, err := opts.toAPIBuildOptions(services)
if err != nil {
return err
}
apiBuildOptions.Attestations = true
return backend.Build(ctx, project, apiBuildOptions)
}
================================================
FILE: cmd/compose/commit.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type commitOptions struct {
*ProjectOptions
service string
reference string
pause bool
comment string
author string
changes opts.ListOpts
index int
}
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := commitOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]",
Short: "Create a new image from a service container's changes",
Args: cobra.RangeArgs(1, 2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
options.service = args[0]
if len(args) > 1 {
options.reference = args[1]
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runCommit(ctx, dockerCli, backendOptions, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith ")`)
options.changes = opts.NewListOpts(nil)
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
return cmd
}
func runCommit(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options commitOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Commit(ctx, projectName, api.CommitOptions{
Service: options.service,
Reference: options.reference,
Pause: options.pause,
Comment: options.comment,
Author: options.author,
Changes: options.changes,
Index: options.index,
})
}
================================================
FILE: cmd/compose/completion.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"sort"
"strings"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
// validArgsFn defines a completion func to be returned to fetch completion options
type validArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
func noCompletion() validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveNoSpace
}
}
func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var values []string
serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...)
for _, s := range serviceNames {
if toComplete == "" || strings.HasPrefix(s, toComplete) {
values = append(values, s)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
list, err := backend.List(cmd.Context(), api.ListOptions{
All: true,
})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var values []string
for _, stack := range list {
if strings.HasPrefix(stack.Name, toComplete) {
values = append(values, stack.Name)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
allProfileNames := project.AllServices().GetProfiles()
sort.Strings(allProfileNames)
var values []string
for _, profileName := range allProfileNames {
if strings.HasPrefix(profileName, toComplete) {
values = append(values, profileName)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeScaleArgs(cli command.Cli, p *ProjectOptions) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
completions, directive := completeServiceNames(cli, p)(cmd, args, toComplete)
for i, completion := range completions {
completions[i] = completion + "="
}
return completions, directive
}
}
================================================
FILE: cmd/compose/compose.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader"
composepaths "github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
composegoutils "github.com/compose-spec/compose-go/v2/utils"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/pkg/kvfile"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/remote"
"github.com/docker/compose/v5/pkg/utils"
)
const (
// ComposeParallelLimit set the limit running concurrent operation on docker engine
ComposeParallelLimit = "COMPOSE_PARALLEL_LIMIT"
// ComposeProjectName define the project name to be used, instead of guessing from parent directory
ComposeProjectName = "COMPOSE_PROJECT_NAME"
// ComposeCompatibility try to mimic compose v1 as much as possible
ComposeCompatibility = api.ComposeCompatibility
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
// ComposeIgnoreOrphans ignore "orphaned" containers
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
// ComposeEnvFiles defines the env files to use if --env-file isn't used
ComposeEnvFiles = "COMPOSE_ENV_FILES"
// ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
ComposeMenu = "COMPOSE_MENU"
// ComposeProgress defines type of progress output, if --progress isn't used
ComposeProgress = "COMPOSE_PROGRESS"
)
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
lines, err := kvfile.ParseFromReader(r, lookup)
if err != nil {
return fmt.Errorf("failed to parse env_file %s: %w", filename, err)
}
for _, line := range lines {
key, value, _ := strings.Cut(line, "=")
vars[key] = value
}
return nil
}
var stdioToStdout bool
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)
if v, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT"); ok {
stdioToStdout, _ = strconv.ParseBool(v)
}
}
// Command defines a compose CLI command as a func with args
type Command func(context.Context, []string) error
// CobraCommand defines a cobra command function
type CobraCommand func(context.Context, *cobra.Command, []string) error
// AdaptCmd adapt a CobraCommand func to cobra library
func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-s
cancel()
signal.Stop(s)
close(s)
}()
err := fn(ctx, cmd, args)
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
err = dockercli.StatusError{
StatusCode: 130,
}
}
if display.Mode == display.ModeJSON {
err = makeJSONError(err)
}
return err
}
}
// Adapt a Command func to cobra library
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
return AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
return fn(ctx, args)
})
}
type ProjectOptions struct {
ProjectName string
Profiles []string
ConfigPaths []string
WorkDir string
ProjectDir string
EnvFiles []string
Compatibility bool
Progress string
Offline bool
All bool
insecureRegistries []string
}
// ProjectFunc does stuff within a types.Project
type ProjectFunc func(ctx context.Context, project *types.Project) error
// ProjectServicesFunc does stuff within a types.Project and a selection of services
type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
// WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func(cmd *cobra.Command, args []string) error {
return o.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return fn(ctx, project)
})
}
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
return Adapt(func(ctx context.Context, services []string) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, metrics, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
return fn(ctx, project, services)
})
}
type jsonErrorData struct {
Error bool `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func errorAsJSON(message string) string {
errorMessage := &jsonErrorData{
Error: true,
Message: message,
}
marshal, err := json.Marshal(errorMessage)
if err == nil {
return string(marshal)
} else {
return message
}
}
func makeJSONError(err error) error {
if err == nil {
return nil
}
var statusErr dockercli.StatusError
if errors.As(err, &statusErr) {
return dockercli.StatusError{
StatusCode: statusErr.StatusCode,
Status: errorAsJSON(statusErr.Status),
}
}
return fmt.Errorf("%s", errorAsJSON(err.Error()))
}
func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringArrayVar(&o.insecureRegistries, "insecure-registry", []string{}, "Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images")
_ = f.MarkHidden("insecure-registry")
f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file")
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
_ = f.MarkHidden("workdir")
}
// get default value for a command line flag that is set by a coma-separated value in environment variable
func defaultStringArrayVar(env string) []string {
return strings.FieldsFunc(os.Getenv(env), func(c rune) bool {
return c == ','
})
}
func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
name := o.ProjectName
var project *types.Project
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, "", err
}
p, _, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
return nil, envProjectName, nil
}
return nil, "", err
}
project = p
name = p.Name
}
return project, name, nil
}
func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
if o.ProjectName != "" {
return o.ProjectName, nil
}
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
return envProjectName, nil
}
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return "", err
}
project, _, err := o.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return "", err
}
return project.Name, nil
}
func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
remotes := o.remoteLoaders(dockerCli)
for _, r := range remotes {
po = append(po, cli.WithResourceLoader(r))
}
options, err := o.toProjectOptions(po...)
if err != nil {
return nil, err
}
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_"
}
return options.LoadModel(ctx)
}
// ToProject loads a Compose project using the LoadProject API.
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
var metrics tracing.Metrics
remotes := o.remoteLoaders(dockerCli)
// Setup metrics listener to collect project data
metricsListener := func(event string, metadata map[string]any) {
switch event {
case "extends":
metrics.CountExtends++
case "include":
paths := metadata["path"].(types.StringList)
for _, path := range paths {
var isRemote bool
for _, r := range remotes {
if r.Accept(path) {
isRemote = true
break
}
}
if isRemote {
metrics.CountIncludesRemote++
} else {
metrics.CountIncludesLocal++
}
}
}
}
loadOpts := api.ProjectLoadOptions{
ProjectName: o.ProjectName,
ConfigPaths: o.ConfigPaths,
WorkingDir: o.ProjectDir,
EnvFiles: o.EnvFiles,
Profiles: o.Profiles,
Services: services,
Offline: o.Offline,
All: o.All,
Compatibility: o.Compatibility,
ProjectOptionsFns: po,
LoadListeners: []api.LoadListener{metricsListener},
OCI: api.OCIOptions{
InsecureRegistries: o.insecureRegistries,
},
}
project, err := backend.LoadProject(ctx, loadOpts)
if err != nil {
return nil, metrics, err
}
return project, metrics, nil
}
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
if o.Offline {
return nil
}
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline, api.OCIOptions{})
return []loader.ResourceLoader{git, oci}
}
func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
opts := []cli.ProjectOptionsFn{
cli.WithWorkingDirectory(o.ProjectDir),
// First apply os.Environment, always win
cli.WithOsEnv,
}
if _, present := os.LookupEnv("PWD"); !present {
if pwd, err := os.Getwd(); err != nil {
return nil, err
} else {
opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
}
}
opts = append(opts,
// Load PWD/.env if present and no explicit --env-file has been set
cli.WithEnvFiles(o.EnvFiles...),
// read dot env file to populate project environment
cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
cli.WithDefaultConfigPath,
// .. and then, a project directory != PWD maybe has been set so let's load .env file
cli.WithEnvFiles(o.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
cli.WithDotEnv, //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
// eventually COMPOSE_PROFILES should have been set
cli.WithDefaultProfiles(o.Profiles...),
cli.WithName(o.ProjectName),
)
return cli.NewProjectOptions(o.ConfigPaths, append(po, opts...)...)
}
// PluginName is the name of the plugin
const PluginName = "compose"
// RunningAsStandalone detects when running as a standalone program
func RunningAsStandalone() bool {
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
}
type BackendOptions struct {
Options []compose.Option
}
func (o *BackendOptions) Add(option compose.Option) {
o.Options = append(o.Options, option)
}
// RootCommand returns the compose command with its child commands
func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
opts := ProjectOptions{}
var (
ansi string
noAnsi bool
verbose bool
version bool
parallel int
dryRun bool
)
c := &cobra.Command{
Short: "Docker Compose",
Long: "Define and run multi-container applications with Docker",
Use: PluginName,
TraverseChildren: true,
// By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
if version {
return versionCommand(dockerCli).Execute()
}
_ = cmd.Help()
return dockercli.StatusError{
StatusCode: 1,
Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
}
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
parent := cmd.Root()
if parent != nil {
parentPrerun := parent.PersistentPreRunE
if parentPrerun != nil {
err := parentPrerun(cmd, args)
if err != nil {
return err
}
}
}
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}
err := setEnvWithDotEnv(opts, dockerCli)
if err != nil {
return err
}
if noAnsi {
if ansi != "auto" {
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
}
ansi = "never"
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
}
if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
ansi = v
}
formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
display.NoColor()
formatter.SetANSIMode(dockerCli, formatter.Never)
}
switch ansi {
case "never":
display.Mode = display.ModePlain
case "always":
display.Mode = display.ModeTTY
}
detached, _ := cmd.Flags().GetBool("detach")
var ep api.EventProcessor
switch opts.Progress {
case "", display.ModeAuto:
switch {
case ansi == "never":
display.Mode = display.ModePlain
ep = display.Plain(dockerCli.Err())
case dockerCli.Out().IsTerminal():
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
default:
ep = display.Plain(dockerCli.Err())
}
case display.ModeTTY:
if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
}
display.Mode = display.ModeTTY
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
case display.ModePlain:
if ansi == "always" {
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
}
display.Mode = display.ModePlain
ep = display.Plain(dockerCli.Err())
case display.ModeQuiet, "none":
display.Mode = display.ModeQuiet
ep = display.Quiet()
case display.ModeJSON:
display.Mode = display.ModeJSON
logrus.SetFormatter(&logrus.JSONFormatter{})
ep = display.JSON(dockerCli.Err())
default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
}
backendOptions.Add(compose.WithEventProcessor(ep))
// (4) options validation / normalization
if opts.WorkDir != "" {
if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
}
opts.ProjectDir = opts.WorkDir
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
}
for i, file := range opts.EnvFiles {
file = composepaths.ExpandUser(file)
if !filepath.IsAbs(file) {
file, err := filepath.Abs(file)
if err != nil {
return err
}
opts.EnvFiles[i] = file
} else {
opts.EnvFiles[i] = file
}
}
composeCmd := cmd
for composeCmd.Name() != PluginName {
if !composeCmd.HasParent() {
return fmt.Errorf("error parsing command line, expected %q", PluginName)
}
composeCmd = composeCmd.Parent()
}
if v, ok := os.LookupEnv(ComposeParallelLimit); ok && !composeCmd.Flags().Changed("parallel") {
i, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("%s must be an integer (found: %q)", ComposeParallelLimit, v)
}
parallel = i
}
if parallel > 0 {
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
backendOptions.Add(compose.WithMaxConcurrency(parallel))
}
// dry run detection
if dryRun {
backendOptions.Add(compose.WithDryRun)
}
return nil
},
}
c.AddCommand(
upCommand(&opts, dockerCli, backendOptions),
downCommand(&opts, dockerCli, backendOptions),
startCommand(&opts, dockerCli, backendOptions),
restartCommand(&opts, dockerCli, backendOptions),
stopCommand(&opts, dockerCli, backendOptions),
psCommand(&opts, dockerCli, backendOptions),
listCommand(dockerCli, backendOptions),
logsCommand(&opts, dockerCli, backendOptions),
configCommand(&opts, dockerCli),
killCommand(&opts, dockerCli, backendOptions),
runCommand(&opts, dockerCli, backendOptions),
removeCommand(&opts, dockerCli, backendOptions),
execCommand(&opts, dockerCli, backendOptions),
attachCommand(&opts, dockerCli, backendOptions),
exportCommand(&opts, dockerCli, backendOptions),
commitCommand(&opts, dockerCli, backendOptions),
pauseCommand(&opts, dockerCli, backendOptions),
unpauseCommand(&opts, dockerCli, backendOptions),
topCommand(&opts, dockerCli, backendOptions),
eventsCommand(&opts, dockerCli, backendOptions),
portCommand(&opts, dockerCli, backendOptions),
imagesCommand(&opts, dockerCli, backendOptions),
versionCommand(dockerCli),
buildCommand(&opts, dockerCli, backendOptions),
pushCommand(&opts, dockerCli, backendOptions),
pullCommand(&opts, dockerCli, backendOptions),
createCommand(&opts, dockerCli, backendOptions),
copyCommand(&opts, dockerCli, backendOptions),
waitCommand(&opts, dockerCli, backendOptions),
scaleCommand(&opts, dockerCli, backendOptions),
statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backendOptions),
publishCommand(&opts, dockerCli, backendOptions),
alphaCommand(&opts, dockerCli, backendOptions),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backendOptions),
)
c.Flags().SetInterspersed(false)
opts.addProjectFlags(c.Flags())
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-name",
completeProjectNames(dockerCli, backendOptions),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-directory",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveFilterDirs
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"file",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"profile",
completeProfileNames(dockerCli, &opts),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"progress",
cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
)
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
c.Flags().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
c.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Execute command in dry run mode")
c.Flags().MarkHidden("version") //nolint:errcheck
c.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
c.Flags().MarkHidden("no-ansi") //nolint:errcheck
c.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
c.Flags().MarkHidden("verbose") //nolint:errcheck
return c
}
func stdinfo(dockerCli command.Cli) io.Writer {
if stdioToStdout {
return dockerCli.Out()
}
return dockerCli.Err()
}
func setEnvWithDotEnv(opts ProjectOptions, dockerCli command.Cli) error {
// Check if we're using a remote config (OCI or Git)
// If so, skip env loading as remote loaders haven't been initialized yet
// and trying to process the path would fail
remoteLoaders := opts.remoteLoaders(dockerCli)
for _, path := range opts.ConfigPaths {
for _, loader := range remoteLoaders {
if loader.Accept(path) {
// Remote config - skip env loading for now
// It will be loaded later when the project is fully initialized
return nil
}
}
}
options, err := cli.NewProjectOptions(opts.ConfigPaths,
cli.WithWorkingDirectory(opts.ProjectDir),
cli.WithOsEnv,
cli.WithEnvFiles(opts.EnvFiles...),
cli.WithDotEnv,
)
if err != nil {
return err
}
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
if err != nil {
return err
}
for k, v := range envFromFile {
if _, ok := os.LookupEnv(k); !ok && strings.HasPrefix(k, "COMPOSE_") {
if err := os.Setenv(k, v); err != nil {
return err
}
}
}
return nil
}
var printerModes = []string{
display.ModeAuto,
display.ModeTTY,
display.ModePlain,
display.ModeJSON,
display.ModeQuiet,
}
================================================
FILE: cmd/compose/compose_oci_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestSetEnvWithDotEnv_WithOCIArtifact(t *testing.T) {
// Test that setEnvWithDotEnv doesn't fail when using OCI artifacts
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"oci://docker.io/dockersamples/welcome-to-docker"},
ProjectDir: "",
EnvFiles: []string{},
}
err := setEnvWithDotEnv(opts, cli)
assert.NilError(t, err, "setEnvWithDotEnv should not fail with OCI artifact path")
}
func TestSetEnvWithDotEnv_WithGitRemote(t *testing.T) {
// Test that setEnvWithDotEnv doesn't fail when using Git remotes
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"https://github.com/docker/compose.git"},
ProjectDir: "",
EnvFiles: []string{},
}
err := setEnvWithDotEnv(opts, cli)
assert.NilError(t, err, "setEnvWithDotEnv should not fail with Git remote path")
}
func TestSetEnvWithDotEnv_WithLocalPath(t *testing.T) {
// Test that setEnvWithDotEnv still works with local paths
// This will fail if the file doesn't exist, but it should not panic
// or produce invalid paths
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"compose.yaml"},
ProjectDir: "",
EnvFiles: []string{},
}
// This may error if files don't exist, but should not panic
_ = setEnvWithDotEnv(opts, cli)
}
================================================
FILE: cmd/compose/compose_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestFilterServices(t *testing.T) {
p := &types.Project{
Services: types.Services{
"foo": {
Name: "foo",
Links: []string{"bar"},
},
"bar": {
Name: "bar",
DependsOn: map[string]types.ServiceDependency{
"zot": {},
},
},
"zot": {
Name: "zot",
},
"qix": {
Name: "qix",
},
},
}
p, err := p.WithSelectedServices([]string{"bar"})
assert.NilError(t, err)
assert.Equal(t, len(p.Services), 2)
_, err = p.GetService("bar")
assert.NilError(t, err)
_, err = p.GetService("zot")
assert.NilError(t, err)
}
================================================
FILE: cmd/compose/config.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"sort"
"strings"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"go.yaml.in/yaml/v4"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type configOptions struct {
*ProjectOptions
Format string
Output string
quiet bool
resolveImageDigests bool
noInterpolate bool
noNormalize bool
noResolvePath bool
noResolveEnv bool
services bool
volumes bool
networks bool
models bool
profiles bool
images bool
hash string
noConsistency bool
variables bool
environment bool
lockImageDigests bool
}
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string) (*types.Project, error) {
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, o.toProjectOptionsFns()...)
return project, err
}
func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
po = append(po, o.toProjectOptionsFns()...)
return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
}
// toProjectOptionsFns converts config options to cli.ProjectOptionsFn
func (o *configOptions) toProjectOptionsFns() []cli.ProjectOptionsFn {
fns := []cli.ProjectOptionsFn{
cli.WithInterpolation(!o.noInterpolate),
cli.WithResolvedPaths(!o.noResolvePath),
cli.WithNormalization(!o.noNormalize),
cli.WithConsistency(!o.noConsistency),
cli.WithDefaultProfiles(o.Profiles...),
cli.WithDiscardEnvFile,
}
if o.noResolveEnv {
fns = append(fns, cli.WithoutEnvironmentResolution)
}
return fns
}
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
opts := configOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "config [OPTIONS] [SERVICE...]",
Short: "Parse, resolve and render compose file in canonical format",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
if p.Compatibility {
opts.noNormalize = true
}
if opts.lockImageDigests {
opts.resolveImageDigests = true
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
if opts.services {
return runServices(ctx, dockerCli, opts)
}
if opts.volumes {
return runVolumes(ctx, dockerCli, opts)
}
if opts.networks {
return runNetworks(ctx, dockerCli, opts)
}
if opts.models {
return runModels(ctx, dockerCli, opts)
}
if opts.hash != "" {
return runHash(ctx, dockerCli, opts)
}
if opts.profiles {
return runProfiles(ctx, dockerCli, opts, args)
}
if opts.images {
return runConfigImages(ctx, dockerCli, opts, args)
}
if opts.variables {
return runVariables(ctx, dockerCli, opts, args)
}
if opts.environment {
return runEnvironment(ctx, dockerCli, opts, args)
}
if opts.Format == "" {
opts.Format = "yaml"
}
return runConfig(ctx, dockerCli, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]")
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything")
flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables")
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model")
flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths")
flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
flags.BoolVar(&opts.noResolveEnv, "no-env-resolution", false, "Don't resolve service env files")
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
flags.BoolVar(&opts.models, "models", false, "Print the model names, one per line.")
flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
flags.BoolVar(&opts.variables, "variables", false, "Print model variables and default values.")
flags.BoolVar(&opts.environment, "environment", false, "Print environment used for interpolation.")
flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
return cmd
}
func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) (err error) {
var content []byte
if opts.noInterpolate {
content, err = runConfigNoInterpolate(ctx, dockerCli, opts, services)
if err != nil {
return err
}
} else {
content, err = runConfigInterpolate(ctx, dockerCli, opts, services)
if err != nil {
return err
}
}
if !opts.noInterpolate {
content = escapeDollarSign(content)
}
if opts.quiet {
return nil
}
if opts.Output != "" && len(content) > 0 {
return os.WriteFile(opts.Output, content, 0o666)
}
_, err = fmt.Fprint(dockerCli.Out(), string(content))
return err
}
func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return nil, err
}
if opts.resolveImageDigests {
project, err = project.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
if err != nil {
return nil, err
}
}
if !opts.noResolveEnv {
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return nil, err
}
}
if !opts.noConsistency {
err := project.CheckContainerNameUnicity()
if err != nil {
return nil, err
}
}
if opts.lockImageDigests {
project = imagesOnly(project)
}
var content []byte
switch opts.Format {
case "json":
content, err = project.MarshalJSON()
case "yaml":
content, err = project.MarshalYAML()
default:
return nil, fmt.Errorf("unsupported format %q", opts.Format)
}
if err != nil {
return nil, err
}
return content, nil
}
// imagesOnly return project with all attributes removed but service.images
func imagesOnly(project *types.Project) *types.Project {
digests := types.Services{}
for name, config := range project.Services {
digests[name] = types.ServiceConfig{
Image: config.Image,
}
}
project = &types.Project{Services: digests}
return project
}
func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
// we can't use ToProject, so the model we render here is only partially resolved
model, err := opts.ToModel(ctx, dockerCli, services)
if err != nil {
return nil, err
}
if opts.resolveImageDigests {
err = resolveImageDigests(ctx, dockerCli, model)
if err != nil {
return nil, err
}
}
if opts.lockImageDigests {
for key, e := range model {
if key != "services" {
delete(model, key)
} else {
for _, s := range e.(map[string]any) {
service := s.(map[string]any)
for key := range service {
if key != "image" {
delete(service, key)
}
}
}
}
}
}
return formatModel(model, opts.Format)
}
func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[string]any) (err error) {
// create a pseudo-project so we can rely on WithImagesResolved to resolve images
p := &types.Project{
Services: types.Services{},
}
services := model["services"].(map[string]any)
for name, s := range services {
service := s.(map[string]any)
if image, ok := service["image"]; ok {
p.Services[name] = types.ServiceConfig{
Image: image.(string),
}
}
}
p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
if err != nil {
return err
}
// Collect image resolved with digest and update model accordingly
for name, s := range services {
service := s.(map[string]any)
config := p.Services[name]
if config.Image != "" {
service["image"] = config.Image
}
services[name] = service
}
model["services"] = services
return nil
}
func formatModel(model map[string]any, format string) (content []byte, err error) {
switch format {
case "json":
return json.MarshalIndent(model, "", " ")
case "yaml":
buf := bytes.NewBuffer([]byte{})
encoder := yaml.NewEncoder(buf)
encoder.SetIndent(2)
err = encoder.Encode(model)
return buf.Bytes(), err
default:
return nil, fmt.Errorf("unsupported format %q", format)
}
}
func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
if opts.noInterpolate {
// we can't use ToProject, so the model we render here is only partially resolved
data, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if _, ok := data["services"]; ok {
for serviceName := range data["services"].(map[string]any) {
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
}
}
return nil
}
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error {
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
return nil
})
return err
}
func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for n := range project.Volumes {
_, _ = fmt.Fprintln(dockerCli.Out(), n)
}
return nil
}
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for n := range project.Networks {
_, _ = fmt.Fprintln(dockerCli.Out(), n)
}
return nil
}
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for _, model := range project.Models {
if model.Model != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
}
}
return nil
}
func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
var services []string
if opts.hash != "*" {
services = append(services, strings.Split(opts.hash, ",")...)
}
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if err := applyPlatforms(project, true); err != nil {
return err
}
if len(services) == 0 {
services = project.ServiceNames()
}
sorted := services
slices.Sort(sorted)
for _, name := range sorted {
s, err := project.GetService(name)
if err != nil {
return err
}
hash, err := compose.ServiceHash(s)
if err != nil {
return err
}
_, _ = fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash)
}
return nil
}
func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
set := map[string]struct{}{}
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
for _, s := range project.AllServices() {
for _, p := range s.Profiles {
set[p] = struct{}{}
}
}
profiles := make([]string, 0, len(set))
for p := range set {
profiles = append(profiles, p)
}
sort.Strings(profiles)
for _, p := range profiles {
_, _ = fmt.Fprintln(dockerCli.Out(), p)
}
return nil
}
func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
for _, s := range project.Services {
_, _ = fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name))
}
return nil
}
func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
opts.noInterpolate = true
model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
variables := template.ExtractVariables(model, template.DefaultPattern)
if opts.Format == "yaml" {
result, err := yaml.Marshal(variables)
if err != nil {
return err
}
fmt.Print(string(result))
return nil
}
return formatter.Print(variables, opts.Format, dockerCli.Out(), func(w io.Writer) {
for name, variable := range variables {
_, _ = fmt.Fprintf(w, "%s\t%t\t%s\t%s\n", name, variable.Required, variable.DefaultValue, variable.PresenceValue)
}
}, "NAME", "REQUIRED", "DEFAULT VALUE", "ALTERNATE VALUE")
}
func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
for _, v := range project.Environment.Values() {
fmt.Println(v)
}
return nil
}
func escapeDollarSign(marshal []byte) []byte {
dollar := []byte{'$'}
escDollar := []byte{'$', '$'}
return bytes.ReplaceAll(marshal, dollar, escDollar)
}
================================================
FILE: cmd/compose/cp.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type copyOptions struct {
*ProjectOptions
source string
destination string
index int
all bool
followLink bool
copyUIDGID bool
}
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := copyOptions{
ProjectOptions: p,
}
copyCmd := &cobra.Command{
Use: `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH`,
Short: "Copy files/folders between a service container and the local filesystem",
Args: cli.ExactArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if args[0] == "" {
return errors.New("source can not be empty")
}
if args[1] == "" {
return errors.New("destination can not be empty")
}
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.source = args[0]
opts.destination = args[1]
return runCopy(ctx, dockerCli, backendOptions, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := copyCmd.Flags()
flags.IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
flags.BoolVar(&opts.all, "all", false, "Include containers created by the run command")
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
return copyCmd
}
func runCopy(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts copyOptions) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Copy(ctx, name, api.CopyOptions{
Source: opts.source,
Destination: opts.destination,
All: opts.all,
Index: opts.index,
FollowLink: opts.followLink,
CopyUIDGID: opts.copyUIDGID,
})
}
================================================
FILE: cmd/compose/create.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type createOptions struct {
Build bool
noBuild bool
Pull string
pullChanged bool
removeOrphans bool
ignoreOrphans bool
forceRecreate bool
noRecreate bool
recreateDeps bool
noInherit bool
timeChanged bool
timeout int
quietPull bool
scale []string
AssumeYes bool
}
func createCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "create [OPTIONS] [SERVICE...]",
Short: "Creates containers for a service",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.pullChanged = cmd.Flags().Changed("pull")
if opts.Build && opts.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
if opts.forceRecreate && opts.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
}
return nil
}),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return runCreate(ctx, dockerCli, backendOptions, opts, buildOpts, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers")
flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy")
flags.StringVar(&opts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never"|"build")`)
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed")
flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
flags.BoolVarP(&opts.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return cmd
}
func runCreate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
if err := createOpts.Apply(project); err != nil {
return err
}
var build *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(services)
if err != nil {
return err
}
build = &bo
}
if createOpts.AssumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Create(ctx, project, api.CreateOptions{
Build: build,
Services: services,
RemoveOrphans: createOpts.removeOrphans,
IgnoreOrphans: createOpts.ignoreOrphans,
Recreate: createOpts.recreateStrategy(),
RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(),
QuietPull: createOpts.quietPull,
})
}
func (opts createOptions) recreateStrategy() string {
if opts.noRecreate {
return api.RecreateNever
}
if opts.forceRecreate {
return api.RecreateForce
}
if opts.noInherit {
return api.RecreateForce
}
return api.RecreateDiverged
}
func (opts createOptions) dependenciesRecreateStrategy() string {
if opts.noRecreate {
return api.RecreateNever
}
if opts.recreateDeps {
return api.RecreateForce
}
return api.RecreateDiverged
}
func (opts createOptions) GetTimeout() *time.Duration {
if opts.timeChanged {
t := time.Duration(opts.timeout) * time.Second
return &t
}
return nil
}
func (opts createOptions) Apply(project *types.Project) error {
if opts.pullChanged {
if !opts.isPullPolicyValid() {
return fmt.Errorf("invalid --pull option %q", opts.Pull)
}
for i, service := range project.Services {
service.PullPolicy = opts.Pull
project.Services[i] = service
}
}
// N.B. opts.Build means "force build all", but images can still be built
// when this is false
// e.g. if a service has pull_policy: build or its local image is policy
if opts.Build {
for i, service := range project.Services {
if service.Build == nil {
continue
}
service.PullPolicy = types.PullPolicyBuild
project.Services[i] = service
}
}
if err := applyPlatforms(project, true); err != nil {
return err
}
err := applyScaleOpts(project, opts.scale)
if err != nil {
return err
}
return nil
}
func applyScaleOpts(project *types.Project, opts []string) error {
for _, scale := range opts {
name, val, ok := strings.Cut(scale, "=")
if !ok || val == "" {
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
}
replicas, err := strconv.Atoi(val)
if err != nil {
return err
}
err = setServiceScale(project, name, replicas)
if err != nil {
return err
}
}
return nil
}
func (opts createOptions) isPullPolicyValid() bool {
pullPolicies := []string{
types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild,
types.PullPolicyMissing, types.PullPolicyIfNotPresent,
}
return slices.Contains(pullPolicies, opts.Pull)
}
================================================
FILE: cmd/compose/down.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"time"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type downOptions struct {
*ProjectOptions
removeOrphans bool
timeChanged bool
timeout int
volumes bool
images string
}
func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := downOptions{
ProjectOptions: p,
}
downCmd := &cobra.Command{
Use: "down [OPTIONS] [SERVICES]",
Short: "Stop and remove containers, networks",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
if opts.images != "" {
if opts.images != "all" && opts.images != "local" {
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
}
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runDown(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := downCmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers`)
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "volume" {
name = "volumes"
logrus.Warn("--volume is deprecated, please use --volumes")
}
return pflag.NormalizedName(name)
})
return downCmd
}
func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Down(ctx, name, api.DownOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,
Timeout: timeout,
Images: opts.images,
Volumes: opts.volumes,
Services: services,
})
}
================================================
FILE: cmd/compose/events.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type eventsOpts struct {
*composeOptions
json bool
since string
until string
}
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := eventsOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
cmd := &cobra.Command{
Use: "events [OPTIONS] [SERVICE...]",
Short: "Receive real time events from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runEvents(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
cmd.Flags().StringVar(&opts.since, "since", "", "Show all events created since timestamp")
cmd.Flags().StringVar(&opts.until, "until", "", "Stream events until this timestamp")
return cmd
}
func runEvents(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts eventsOpts, services []string) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Events(ctx, name, api.EventsOptions{
Services: services,
Since: opts.since,
Until: opts.until,
Consumer: func(event api.Event) error {
if opts.json {
marshal, err := json.Marshal(map[string]any{
"time": event.Timestamp,
"type": "container",
"service": event.Service,
"id": event.Container,
"action": event.Status,
"attributes": event.Attributes,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCli.Out(), string(marshal))
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), event)
}
return nil
},
})
}
================================================
FILE: cmd/compose/exec.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type execOpts struct {
*composeOptions
service string
command []string
environment []string
workingDir string
noTty bool
user string
detach bool
index int
privileged bool
interactive bool
}
func execCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := execOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]",
Short: "Execute a command in a running container",
Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0]
opts.command = args[1:]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
err := runExec(ctx, dockerCli, backendOptions, opts)
if err != nil {
logrus.Debugf("%v", err)
var cliError cli.StatusError
if ok := errors.As(err, &cliError); ok {
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
}
}
return err
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background")
runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user")
runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command")
runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY")
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false)
runCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "no-TTY" { // legacy
name = "no-tty"
}
return pflag.NormalizedName(name)
})
return runCmd
}
func runExec(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts execOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
projectOptions, err := opts.composeOptions.toProjectOptions() //nolint:staticcheck
if err != nil {
return err
}
lookupFn := func(k string) (string, bool) {
v, ok := projectOptions.Environment[k]
return v, ok
}
execOpts := api.RunOptions{
Service: opts.service,
Command: opts.command,
Environment: compose.ToMobyEnv(types.NewMappingWithEquals(opts.environment).Resolve(lookupFn)),
Tty: !opts.noTty,
User: opts.user,
Privileged: opts.privileged,
Index: opts.index,
Detach: opts.detach,
WorkingDir: opts.workingDir,
Interactive: opts.interactive,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 {
errMsg := fmt.Sprintf("exit status %d", exitCode)
if err != nil && err.Error() != "" {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
}
return err
}
================================================
FILE: cmd/compose/export.go
================================================
/*
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/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type exportOptions struct {
*ProjectOptions
service string
output string
index int
}
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *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, backendOptions, 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, backendOptions *BackendOptions, 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,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Export(ctx, projectName, exportOptions)
}
================================================
FILE: cmd/compose/generate.go
================================================
/*
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/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type generateOptions struct {
*ProjectOptions
Format string
}
func generateCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *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, dockerCli, backendOptions, 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, dockerCli command.Cli, backendOptions *BackendOptions, 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")
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
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
}
================================================
FILE: cmd/compose/images.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"maps"
"slices"
"strings"
"time"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/go-units"
"github.com/moby/moby/client/pkg/stringid"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type imageOptions struct {
*ProjectOptions
Quiet bool
Format string
}
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := imageOptions{
ProjectOptions: p,
}
imgCmd := &cobra.Command{
Use: "images [OPTIONS] [SERVICE...]",
Short: "List images used by the created containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runImages(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return imgCmd
}
func runImages(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts imageOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
images, err := backend.Images(ctx, projectName, api.ImagesOptions{
Services: services,
})
if err != nil {
return err
}
if opts.Quiet {
ids := []string{}
for _, img := range images {
id := img.ID
if i := strings.IndexRune(img.ID, ':'); i >= 0 {
id = id[i+1:]
}
if !slices.Contains(ids, id) {
ids = append(ids, id)
}
}
for _, img := range ids {
_, _ = fmt.Fprintln(dockerCli.Out(), img)
}
return nil
}
if opts.Format == "json" {
type img struct {
ID string `json:"ID"`
ContainerName string `json:"ContainerName"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
Created *time.Time `json:"Created,omitempty"`
LastTagTime time.Time `json:"LastTagTime,omitzero"`
}
// Convert map to slice
var imageList []img
for ctr, i := range images {
lastTagTime := i.LastTagTime
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
Repository: i.Repository,
Tag: i.Tag,
Platform: platforms.Format(i.Platform),
Size: i.Size,
Created: i.Created,
LastTagTime: lastTagTime,
})
}
json, err := formatter.ToJSON(imageList, "", "")
if err != nil {
return err
}
_, err = fmt.Fprintln(dockerCli.Out(), json)
return err
}
return formatter.Print(images, opts.Format, dockerCli.Out(),
func(w io.Writer) {
for _, container := range slices.Sorted(maps.Keys(images)) {
img := images[container]
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo := img.Repository
if repo == "" {
repo = ""
}
tag := img.Tag
if tag == "" {
tag = ""
}
created := "N/A"
if img.Created != nil {
created = units.HumanDuration(time.Now().UTC().Sub(*img.Created)) + " ago"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
container, repo, tag, platforms.Format(img.Platform), id, size, created)
}
},
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED")
}
================================================
FILE: cmd/compose/kill.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"os"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type killOptions struct {
*ProjectOptions
removeOrphans bool
signal string
}
func killCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := killOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "kill [OPTIONS] [SERVICE...]",
Short: "Force stop service containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runKill(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file")
flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container")
return cmd
}
func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts killOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
err = backend.Kill(ctx, name, api.KillOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,
Services: services,
Signal: opts.signal,
})
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill")
return nil
}
return err
}
================================================
FILE: cmd/compose/list.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"io"
"regexp"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type lsOptions struct {
Format string
Quiet bool
All bool
Filter opts.FilterOpt
}
func listCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
lsCmd := &cobra.Command{
Use: "ls [OPTIONS]",
Short: "List running compose projects",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runList(ctx, dockerCli, backendOptions, lsOpts)
}),
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(),
}
lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json]")
lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display project names")
lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided")
lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects")
return lsCmd
}
var acceptedListFilters = map[string]bool{
"name": true,
}
// match returns true if any of the values at key match the source string
func match(filters client.Filters, field, source string) bool {
if f, ok := filters[field]; ok && f[source] {
return true
}
fieldValues := filters[field]
for name2match := range fieldValues {
isMatch, err := regexp.MatchString(name2match, source)
if err != nil {
continue
}
if isMatch {
return true
}
}
return false
}
func runList(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, lsOpts lsOptions) error {
filters := lsOpts.Filter.Value()
for filter := range filters {
if _, ok := acceptedListFilters[filter]; !ok {
return errors.New("invalid filter '" + filter + "'")
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
if err != nil {
return err
}
if len(filters) > 0 {
var filtered []api.Stack
for _, s := range stackList {
if match(filters, "name", s.Name) {
filtered = append(filtered, s)
}
}
stackList = filtered
}
if lsOpts.Quiet {
for _, s := range stackList {
_, _ = fmt.Fprintln(dockerCli.Out(), s.Name)
}
return nil
}
view := viewFromStackList(stackList)
return formatter.Print(view, lsOpts.Format, dockerCli.Out(), func(w io.Writer) {
for _, stack := range view {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles)
}
}, "NAME", "STATUS", "CONFIG FILES")
}
type stackView struct {
Name string
Status string
ConfigFiles string
}
func viewFromStackList(stackList []api.Stack) []stackView {
retList := make([]stackView, len(stackList))
for i, s := range stackList {
retList[i] = stackView{
Name: s.Name,
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
ConfigFiles: s.ConfigFiles,
}
}
return retList
}
================================================
FILE: cmd/compose/logs.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type logsOptions struct {
*ProjectOptions
composeOptions
follow bool
index int
tail string
since string
until string
noColor bool
noPrefix bool
timestamps bool
}
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := logsOptions{
ProjectOptions: p,
}
logsCmd := &cobra.Command{
Use: "logs [OPTIONS] [SERVICE...]",
Short: "View output from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runLogs(ctx, dockerCli, backendOptions, opts, args)
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.index > 0 && len(args) != 1 {
return errors.New("--index requires one service to be selected")
}
return nil
},
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := logsCmd.Flags()
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output")
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs")
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container")
return logsCmd
}
func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts logsOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
// exclude services configured to ignore output (attach: false), until explicitly selected
if project != nil && len(services) == 0 {
for n, service := range project.Services {
if service.Attach == nil || *service.Attach {
services = append(services, n)
}
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project,
Services: services,
Follow: opts.follow,
Index: opts.index,
Tail: opts.tail,
Since: opts.since,
Until: opts.until,
Timestamps: opts.timestamps,
})
}
var _ api.LogConsumer = &logConsumer{}
type logConsumer struct {
events api.EventProcessor
}
func (l logConsumer) Log(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Text: message,
})
}
func (l logConsumer) Err(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Status: api.Error,
Text: message,
})
}
func (l logConsumer) Status(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Status: api.Error,
Text: message,
})
}
================================================
FILE: cmd/compose/options.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"os"
"slices"
"sort"
"strings"
"text/tabwriter"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/prompt"
"github.com/docker/compose/v5/internal/tracing"
)
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
defaultPlatform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
for name, service := range project.Services {
if service.Build == nil {
continue
}
// default platform only applies if the service doesn't specify
if defaultPlatform != "" && service.Platform == "" {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
}
service.Platform = defaultPlatform
}
if service.Platform != "" {
if len(service.Build.Platforms) > 0 {
if !slices.Contains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
}
}
if buildForSinglePlatform || len(service.Build.Platforms) == 0 {
// if we're building for a single platform, we want to build for the platform we'll use to run the image
// similarly, if no build platforms were explicitly specified, it makes sense to build for the platform
// the image is designed for rather than allowing the builder to infer the platform
service.Build.Platforms = []string{service.Platform}
}
}
// services can specify that they should be built for multiple platforms, which can be used
// with `docker compose build` to produce a multi-arch image
// other cases, such as `up` and `run`, need a single architecture to actually run
// if there is only a single platform present (which might have been inferred
// from service.Platform above), it will be used, even if it requires emulation.
// if there's more than one platform, then the list is cleared so that the builder
// can decide.
// TODO(milas): there's no validation that the platform the builder will pick is actually one
// of the supported platforms from the build definition
// e.g. `build.platforms: [linux/arm64, linux/amd64]` on a `linux/ppc64` machine would build
// for `linux/ppc64` instead of returning an error that it's not a valid platform for the service.
if buildForSinglePlatform && len(service.Build.Platforms) > 1 {
// empty indicates that the builder gets to decide
service.Build.Platforms = nil
}
project.Services[name] = service
}
return nil
}
// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git)
func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool {
if len(options.ConfigPaths) == 0 {
return false
}
remoteLoaders := options.remoteLoaders(dockerCli)
for _, loader := range remoteLoaders {
if loader.Accept(options.ConfigPaths[0]) {
return true
}
}
return false
}
// checksForRemoteStack handles environment variable prompts for remote configurations
func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error {
if !isRemoteConfig(dockerCli, options) {
return nil
}
if metrics, ok := ctx.Value(tracing.MetricsKey{}).(tracing.Metrics); ok && metrics.CountIncludesRemote > 0 {
if err := confirmRemoteIncludes(dockerCli, options, assumeYes); err != nil {
return err
}
}
displayLocationRemoteStack(dockerCli, project, options)
return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
}
// Prepare the values map and collect all variables info
type varInfo struct {
name string
value string
source string
required bool
defaultValue string
}
// promptForInterpolatedVariables displays all variables and their values at once,
// then prompts for confirmation
func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error {
if assumeYes {
return nil
}
varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs)
if err != nil {
return err
}
if noVariables {
return nil
}
displayInterpolationVariables(dockerCli.Out(), varsInfo)
// Prompt for confirmation
userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out())
msg := "\nDo you want to proceed with these variables? [Y/n]: "
confirmed, err := userInput.Confirm(msg, true)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}
func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) {
cmdEnvMap := extractEnvCLIDefined(cmdEnvs)
// Create a model without interpolation to extract variables
opts := configOptions{
noInterpolate: true,
ProjectOptions: projectOptions,
}
model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return nil, false, err
}
// Extract variables that need interpolation
variables := template.ExtractVariables(model, template.DefaultPattern)
if len(variables) == 0 {
return nil, true, nil
}
var varsInfo []varInfo
proposedValues := make(map[string]string)
for name, variable := range variables {
info := varInfo{
name: name,
required: variable.Required,
defaultValue: variable.DefaultValue,
}
// Determine value and source based on priority
if value, exists := cmdEnvMap[name]; exists {
info.value = value
info.source = "command-line"
proposedValues[name] = value
} else if value, exists := os.LookupEnv(name); exists {
info.value = value
info.source = "environment"
proposedValues[name] = value
} else if variable.DefaultValue != "" {
info.value = variable.DefaultValue
info.source = "compose file"
proposedValues[name] = variable.DefaultValue
} else {
info.value = ""
info.source = "none"
}
varsInfo = append(varsInfo, info)
}
return varsInfo, false, nil
}
func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
// Parse command-line environment variables
cmdEnvMap := make(map[string]string)
for _, env := range cmdEnvs {
key, val, ok := strings.Cut(env, "=")
if ok {
cmdEnvMap[key] = val
}
}
return cmdEnvMap
}
func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
// Display all variables in a table format
_, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:")
w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0)
_, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT")
sort.Slice(varsInfo, func(a, b int) bool {
return varsInfo[a].name < varsInfo[b].name
})
for _, info := range varsInfo {
required := "no"
if info.required {
required = "yes"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
info.name,
info.value,
info.source,
required,
info.defaultValue,
)
}
_ = w.Flush()
}
func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck
if display.Mode != display.ModeQuiet && display.Mode != display.ModeJSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
}
}
func confirmRemoteIncludes(dockerCli command.Cli, options buildOptions, assumeYes bool) error {
if assumeYes {
return nil
}
var remoteIncludes []string
remoteLoaders := options.ProjectOptions.remoteLoaders(dockerCli) //nolint:staticcheck
for _, cf := range options.ProjectOptions.ConfigPaths { //nolint:staticcheck
for _, loader := range remoteLoaders {
if loader.Accept(cf) {
remoteIncludes = append(remoteIncludes, cf)
break
}
}
}
if len(remoteIncludes) == 0 {
return nil
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nWarning: This Compose project includes files from remote sources:")
for _, include := range remoteIncludes {
_, _ = fmt.Fprintf(dockerCli.Out(), " - %s\n", include)
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nRemote includes could potentially be malicious. Make sure you trust the source.")
msg := "Do you want to continue? [y/N]: "
confirmed, err := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm(msg, false)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}
================================================
FILE: cmd/compose/options_test.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/streams"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
"alice/32",
},
},
Platform: "alice/32",
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
project.Services["test"].Build.Platforms)
})
}
func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
project.Services["test"].Build.Platforms)
})
}
func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, true),
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, false),
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
}
func TestIsRemoteConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
configPaths []string
want bool
}{
{
name: "empty config paths",
configPaths: []string{},
want: false,
},
{
name: "local file",
configPaths: []string{"docker-compose.yaml"},
want: false,
},
{
name: "OCI reference",
configPaths: []string{"oci://registry.example.com/stack:latest"},
want: true,
},
{
name: "GIT reference",
configPaths: []string{"git://github.com/user/repo.git"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: tt.configPaths,
},
}
got := isRemoteConfig(cli, opts)
require.Equal(t, tt.want, got)
})
}
}
func TestDisplayLocationRemoteStack(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
buf := new(bytes.Buffer)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
project := &types.Project{
Name: "test-project",
WorkingDir: "/tmp/test",
}
options := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
},
}
displayLocationRemoteStack(cli, project, options)
output := buf.String()
require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
}
func TestDisplayInterpolationVariables(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tmpDir := t.TempDir()
// Create a temporary compose file
composeContent := `
services:
app:
image: nginx
environment:
- TEST_VAR=${TEST_VAR:?required} # required with default
- API_KEY=${API_KEY:?} # required without default
- DEBUG=${DEBUG:-true} # optional with default
- UNSET_VAR # optional without default
`
composePath := filepath.Join(tmpDir, "docker-compose.yml")
require.NoError(t, os.WriteFile(composePath, []byte(composeContent), 0o644))
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
// Create ProjectOptions with the temporary compose file
projectOptions := &ProjectOptions{
ConfigPaths: []string{composePath},
}
// Set up the context with necessary environment variables
t.Setenv("TEST_VAR", "test-value")
t.Setenv("API_KEY", "123456")
// Extract variables from the model
info, noVariables, err := extractInterpolationVariablesFromModel(t.Context(), cli, projectOptions, []string{})
require.NoError(t, err)
require.False(t, noVariables)
// Display the variables
displayInterpolationVariables(cli.Out(), info)
// Expected output format with proper spacing
expected := "\nFound the following variables in configuration:\n" +
"VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
"API_KEY 123456 environment yes \n" +
"DEBUG true compose file no true\n" +
"TEST_VAR test-value environment yes \n"
// Normalize spaces and newlines for comparison
normalizeSpaces := func(s string) string {
// Replace multiple spaces with a single space
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
return s
}
actualOutput := buf.String()
// Compare normalized strings
require.Equal(t,
normalizeSpaces(expected),
normalizeSpaces(actualOutput),
"\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
}
func TestConfirmRemoteIncludes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
opts buildOptions
assumeYes bool
userInput string
wantErr bool
errMessage string
wantPrompt bool
wantOutput string
}{
{
name: "no remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"docker-compose.yaml",
"./local/path/compose.yaml",
},
},
},
assumeYes: false,
wantErr: false,
wantPrompt: false,
},
{
name: "assume yes with remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: true,
wantErr: false,
wantPrompt: false,
},
{
name: "user confirms remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: false,
userInput: "y\n",
wantErr: false,
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
" - git://github.com/user/repo.git\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
{
name: "user rejects remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
},
},
},
assumeYes: false,
userInput: "n\n",
wantErr: true,
errMessage: "operation cancelled by user",
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
}
buf := new(bytes.Buffer)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
if tt.wantPrompt {
inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
}
err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
if tt.wantErr {
require.Error(t, err)
require.Equal(t, tt.errMessage, err.Error())
} else {
require.NoError(t, err)
}
if tt.wantOutput != "" {
require.Equal(t, tt.wantOutput, buf.String())
}
buf.Reset()
})
}
}
================================================
FILE: cmd/compose/pause.go
================================================
/*
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/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pauseOptions struct {
*ProjectOptions
}
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pauseOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "pause [SERVICE...]",
Short: "Pause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPause(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Pause(ctx, name, api.PauseOptions{
Services: services,
Project: project,
})
}
type unpauseOptions struct {
*ProjectOptions
}
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := unpauseOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "unpause [SERVICE...]",
Short: "Unpause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runUnPause(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts unpauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.UnPause(ctx, name, api.PauseOptions{
Services: services,
Project: project,
})
}
================================================
FILE: cmd/compose/port.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type portOptions struct {
*ProjectOptions
port uint16
protocol string
index int
}
func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := portOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "port [OPTIONS] SERVICE PRIVATE_PORT",
Short: "Print the public port for a port binding",
Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
port, err := strconv.ParseUint(args[1], 10, 16)
if err != nil {
return err
}
opts.port = uint16(port)
opts.protocol = strings.ToLower(opts.protocol)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPort(ctx, dockerCli, backendOptions, opts, args[0])
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
cmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
return cmd
}
func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts portOptions, service string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
Protocol: opts.protocol,
Index: opts.index,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port)
return nil
}
================================================
FILE: cmd/compose/ps.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strings"
"github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter"
cliflags "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type psOptions struct {
*ProjectOptions
Format string
All bool
Quiet bool
Services bool
Filter string
Status []string
noTrunc bool
Orphans bool
}
func (p *psOptions) parseFilter() error {
if p.Filter == "" {
return nil
}
key, val, ok := strings.Cut(p.Filter, "=")
if !ok {
return errors.New("arguments to --filter should be in form KEY=VAL")
}
switch key {
case "status":
p.Status = append(p.Status, val)
return nil
case "source":
return api.ErrNotImplemented
default:
return fmt.Errorf("unknown filter %s", key)
}
}
func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
psCmd := &cobra.Command{
Use: "ps [OPTIONS] [SERVICE...]",
Short: "List containers",
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.parseFilter()
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPs(ctx, dockerCli, backendOptions, args, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status)")
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
flags.BoolVar(&opts.Services, "services", false, "Display services")
flags.BoolVar(&opts.Orphans, "orphans", true, "Include orphaned services (not declared by project)")
flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
return psCmd
}
func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, opts psOptions) error { //nolint:gocyclo
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
if project != nil {
names := project.ServiceNames()
if len(services) > 0 {
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
} else if !opts.Orphans {
// until user asks to list orphaned services, we only include those declared in project
services = names
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
containers, err := backend.Ps(ctx, name, api.PsOptions{
Project: project,
All: opts.All || len(opts.Status) != 0,
Services: services,
})
if err != nil {
return err
}
if len(opts.Status) != 0 {
containers = filterByStatus(containers, opts.Status)
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
if opts.Quiet {
for _, c := range containers {
_, _ = fmt.Fprintln(dockerCli.Out(), c.ID)
}
return nil
}
if opts.Services {
services := []string{}
for _, c := range containers {
s := c.Service
if !slices.Contains(services, s) {
services = append(services, s)
}
}
_, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
return nil
}
if opts.Format == "" {
opts.Format = dockerCli.ConfigFile().PsFormat
}
containerCtx := cliformatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
Trunc: !opts.noTrunc,
}
return formatter.ContainerWrite(containerCtx, containers)
}
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
var filtered []api.ContainerSummary
for _, c := range containers {
if slices.Contains(statuses, string(c.State)) {
filtered = append(filtered, c)
}
}
return filtered
}
================================================
FILE: cmd/compose/publish.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type publishOptions struct {
*ProjectOptions
resolveImageDigests bool
ociVersion string
withEnvironment bool
assumeYes bool
app bool
insecureRegistry bool
}
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := publishOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backendOptions, opts, args[0])
}),
Args: cli.ExactArgs(1),
}
flags := cmd.Flags()
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
flags.BoolVar(&opts.app, "app", false, "Published compose application (includes referenced images)")
flags.BoolVar(&opts.insecureRegistry, "insecure-registry", false, "Use insecure registry")
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
// Should **only** be used for testing purpose, we don't want to promote use of insecure registries
_ = flags.MarkHidden("insecure-registry")
return cmd
}
func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error {
if opts.assumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}
if metrics.CountIncludesLocal > 0 {
return errors.New("cannot publish compose file with local includes")
}
return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests || opts.app,
Application: opts.app,
OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
InsecureRegistry: opts.insecureRegistry,
})
}
================================================
FILE: cmd/compose/pull.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/morikuni/aec"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pullOptions struct {
*ProjectOptions
composeOptions
quiet bool
parallel bool
noParallel bool
includeDeps bool
ignorePullFailures bool
noBuildable bool
policy string
}
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pullOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "pull [OPTIONS] [SERVICE...]",
Short: "Pull service images",
PreRunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("no-parallel") {
fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
}
if cmd.Flags().Changed("parallel") {
fmt.Fprint(os.Stderr, aec.Apply("option '--parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
}
return nil
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPull(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information")
cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies")
cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel")
flags.MarkHidden("parallel") //nolint:errcheck
cmd.Flags().BoolVar(&opts.noParallel, "no-parallel", true, "DEPRECATED disable parallel pulling")
flags.MarkHidden("no-parallel") //nolint:errcheck
cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures")
cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built")
cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always")`)
return cmd
}
func (opts pullOptions) apply(project *types.Project, services []string) (*types.Project, error) {
if !opts.includeDeps {
var err error
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
if opts.policy != "" {
for i, service := range project.Services {
if service.Image == "" {
continue
}
service.PullPolicy = opts.policy
project.Services[i] = service
}
}
return project, nil
}
func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
project, err = opts.apply(project, services)
if err != nil {
return err
}
return backend.Pull(ctx, project, api.PullOptions{
Quiet: opts.quiet,
IgnoreFailures: opts.ignorePullFailures,
IgnoreBuildable: opts.noBuildable,
})
}
================================================
FILE: cmd/compose/pullOptions_test.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestApplyPullOptions(t *testing.T) {
project := &types.Project{
Services: types.Services{
"must-build": {
Name: "must-build",
// No image, local build only
Build: &types.BuildConfig{
Context: ".",
},
},
"has-build": {
Name: "has-build",
Image: "registry.example.com/myservice",
Build: &types.BuildConfig{
Context: ".",
},
},
"must-pull": {
Name: "must-pull",
Image: "registry.example.com/another-service",
},
},
}
project, err := pullOptions{
policy: types.PullPolicyMissing,
}.apply(project, nil)
assert.NilError(t, err)
assert.Equal(t, project.Services["must-build"].PullPolicy, "") // still default
assert.Equal(t, project.Services["has-build"].PullPolicy, types.PullPolicyMissing)
assert.Equal(t, project.Services["must-pull"].PullPolicy, types.PullPolicyMissing)
}
================================================
FILE: cmd/compose/push.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pushOptions struct {
*ProjectOptions
composeOptions
IncludeDeps bool
Ignorefailures bool
Quiet bool
}
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
pushCmd := &cobra.Command{
Use: "push [OPTIONS] [SERVICE...]",
Short: "Push service images",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPush(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
return pushCmd
}
func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
if !opts.IncludeDeps {
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return err
}
}
return backend.Push(ctx, project, api.PushOptions{
IgnoreFailures: opts.Ignorefailures,
Quiet: opts.Quiet,
})
}
================================================
FILE: cmd/compose/remove.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type removeOptions struct {
*ProjectOptions
force bool
stop bool
volumes bool
}
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := removeOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "rm [OPTIONS] [SERVICE...]",
Short: "Removes stopped service containers",
Long: `Removes stopped service containers
By default, anonymous volumes attached to containers will not be removed. You
can override this with -v. To list all volumes, use "docker volume ls".
Any data which is not in a volume will be lost.`,
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRemove(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
f := cmd.Flags()
f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal")
f.BoolVarP(&opts.stop, "stop", "s", false, "Stop the containers, if required, before removing")
f.BoolVarP(&opts.volumes, "volumes", "v", false, "Remove any anonymous volumes attached to containers")
f.BoolP("all", "a", false, "Deprecated - no effect")
f.MarkHidden("all") //nolint:errcheck
return cmd
}
func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts removeOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
err = backend.Remove(ctx, name, api.RemoveOptions{
Services: services,
Force: opts.force,
Volumes: opts.volumes,
Project: project,
Stop: opts.stop,
})
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No stopped containers")
return nil
}
return err
}
================================================
FILE: cmd/compose/restart.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type restartOptions struct {
*ProjectOptions
timeChanged bool
timeout int
noDeps bool
}
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := restartOptions{
ProjectOptions: p,
}
restartCmd := &cobra.Command{
Use: "restart [OPTIONS] [SERVICE...]",
Short: "Restart service containers",
PreRun: func(cmd *cobra.Command, args []string) {
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRestart(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := restartCmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services")
return restartCmd
}
func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts restartOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return err
}
if project != nil && len(services) > 0 {
project, err = project.WithServicesEnabled(services...)
if err != nil {
return err
}
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Restart(ctx, name, api.RestartOptions{
Timeout: timeout,
Services: services,
Project: project,
NoDeps: opts.noDeps,
})
}
================================================
FILE: cmd/compose/run.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"strings"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/mattn/go-shellwords"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type runOptions struct {
*composeOptions
Service string
Command []string
environment []string
envFiles []string
Detach bool
Remove bool
noTty bool
interactive bool
user string
workdir string
entrypoint string
entrypointCmd []string
capAdd opts.ListOpts
capDrop opts.ListOpts
labels []string
volumes []string
publish []string
useAliases bool
servicePorts bool
name string
noDeps bool
ignoreOrphans bool
removeOrphans bool
quiet bool
quietPull bool
}
func (options runOptions) apply(project *types.Project) (*types.Project, error) {
if options.noDeps {
var err error
project, err = project.WithSelectedServices([]string{options.Service}, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
target, err := project.GetService(options.Service)
if err != nil {
return nil, err
}
target.Tty = !options.noTty
target.StdinOpen = options.interactive
// --service-ports and --publish are incompatible
if !options.servicePorts {
if len(target.Ports) > 0 {
logrus.Debug("Running service without ports exposed as --service-ports=false")
}
target.Ports = []types.ServicePortConfig{}
for _, p := range options.publish {
config, err := types.ParsePortConfig(p)
if err != nil {
return nil, err
}
target.Ports = append(target.Ports, config...)
}
}
for _, v := range options.volumes {
volume, err := format.ParseVolume(v)
if err != nil {
return nil, err
}
target.Volumes = append(target.Volumes, volume)
}
for name := range project.Services {
if name == options.Service {
project.Services[name] = target
break
}
}
return project, nil
}
func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(resolve).ToMapping()
for _, file := range options.envFiles {
f, err := os.Open(file)
if err != nil {
return nil, err
}
vars, err := dotenv.ParseWithLookup(f, func(k string) (string, bool) {
value, ok := environment[k]
return value, ok
})
if err != nil {
return nil, nil
}
for k, v := range vars {
if _, ok := environment[k]; !ok {
environment[k] = v
}
}
}
return environment, nil
}
func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := runOptions{
composeOptions: &composeOptions{
ProjectOptions: p,
},
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
}
createOpts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
}
// We remove the attribute from the option struct and use a dedicated var, to limit confusion and avoid anyone to use options.tty.
// The tty flag is here for convenience and let user do "docker compose run -it" the same way as they use the "docker run" command.
var ttyFlag bool
cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service",
Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
options.Service = args[0]
if len(args) > 1 {
options.Command = args[1:]
}
if len(options.publish) > 0 && options.servicePorts {
return fmt.Errorf("--service-ports and --publish are incompatible")
}
if cmd.Flags().Changed("entrypoint") {
command, err := shellwords.Parse(options.entrypoint)
if err != nil {
return err
}
options.entrypointCmd = command
}
if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
options.noTty = !ttyFlag
}
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !dockerCli.In().IsTerminal() {
// while `docker run` requires explicit `-it` flags, Compose enables interactive mode and TTY by default
// but when compose is used from a script that has stdin piped from another command, we just can't
// Here, we detect we run "by default" (user didn't passed explicit flags) and disable TTY allocation if
// we don't have an actual terminal to attach to for interactive mode
options.noTty = true
}
if options.quiet {
display.Mode = display.ModeQuiet
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
}
createOpts.pullChanged = cmd.Flags().Changed("pull")
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.Service}, composecli.WithoutEnvironmentResolution)
if err != nil {
return err
}
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
if createOpts.quietPull {
buildOpts.Progress = string(xprogress.QuietMode)
}
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVar(&options.envFiles, "env-from-file", []string{}, "Set environment variables from file")
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)")
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services")
flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume")
flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host")
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to")
flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host")
flags.StringVar(&createOpts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process")
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container")
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
cmd.Flags().BoolVarP(&ttyFlag, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags)
flags.SetInterspersed(false)
return cmd
}
func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "volumes":
name = "volume"
case "labels":
name = "label"
}
return pflag.NormalizedName(name)
}
func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
project, err := options.apply(project)
if err != nil {
return err
}
err = createOpts.Apply(project)
if err != nil {
return err
}
if err := checksForRemoteStack(ctx, dockerCli, project, buildOpts, createOpts.AssumeYes, []string{}); err != nil {
return err
}
labels := types.Labels{}
for _, s := range options.labels {
key, val, ok := strings.Cut(s, "=")
if !ok {
return fmt.Errorf("label must be set as KEY=VALUE")
}
labels[key] = val
}
var buildForRun *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
buildForRun = &bo
}
environment, err := options.getEnvironment(project.Environment.Resolve)
if err != nil {
return err
}
// start container and attach to container streams
runOpts := api.RunOptions{
CreateOptions: api.CreateOptions{
Build: buildForRun,
RemoveOrphans: options.removeOrphans,
IgnoreOrphans: options.ignoreOrphans,
QuietPull: options.quietPull,
},
Name: options.name,
Service: options.Service,
Command: options.Command,
Detach: options.Detach,
AutoRemove: options.Remove,
Tty: !options.noTty,
Interactive: options.interactive,
WorkingDir: options.workdir,
User: options.user,
CapAdd: options.capAdd.GetSlice(),
CapDrop: options.capDrop.GetSlice(),
Environment: environment.Values(),
Entrypoint: options.entrypointCmd,
Labels: labels,
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Index: 0,
}
for name, service := range project.Services {
if name == options.Service {
service.StdinOpen = options.interactive
project.Services[name] = service
}
}
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
if exitCode != 0 {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
}
return err
}
================================================
FILE: cmd/compose/scale.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type scaleOptions struct {
*ProjectOptions
noDeps bool
}
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := scaleOptions{
ProjectOptions: p,
}
scaleCmd := &cobra.Command{
Use: "scale [SERVICE=REPLICAS...]",
Short: "Scale services ",
Args: cobra.MinimumNArgs(1),
RunE: Adapt(func(ctx context.Context, args []string) error {
serviceTuples, err := parseServicesReplicasArgs(args)
if err != nil {
return err
}
return runScale(ctx, dockerCli, backendOptions, opts, serviceTuples)
}),
ValidArgsFunction: completeScaleArgs(dockerCli, p),
}
flags := scaleCmd.Flags()
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services")
return scaleCmd
}
func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts scaleOptions, serviceReplicaTuples map[string]int) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
if opts.noDeps {
if project, err = project.WithSelectedServices(services, types.IgnoreDependencies); err != nil {
return err
}
}
for key, value := range serviceReplicaTuples {
service, err := project.GetService(key)
if err != nil {
return err
}
service.SetScale(value)
project.Services[key] = service
}
return backend.Scale(ctx, project, api.ScaleOptions{Services: services})
}
func parseServicesReplicasArgs(args []string) (map[string]int, error) {
serviceReplicaTuples := map[string]int{}
for _, arg := range args {
key, val, ok := strings.Cut(arg, "=")
if !ok || key == "" || val == "" {
return nil, fmt.Errorf("invalid scale specifier: %s", arg)
}
intValue, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("invalid scale specifier: can't parse replica value as int: %v", arg)
}
serviceReplicaTuples[key] = intValue
}
return serviceReplicaTuples, nil
}
================================================
FILE: cmd/compose/start.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type startOptions struct {
*ProjectOptions
wait bool
waitTimeout int
}
func startCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := startOptions{
ProjectOptions: p,
}
startCmd := &cobra.Command{
Use: "start [SERVICE...]",
Short: "Start services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStart(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := startCmd.Flags()
flags.BoolVar(&opts.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&opts.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
return startCmd
}
func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts startOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
var timeout time.Duration
if opts.waitTimeout > 0 {
timeout = time.Duration(opts.waitTimeout) * time.Second
}
return backend.Start(ctx, name, api.StartOptions{
AttachTo: services,
Project: project,
Services: services,
Wait: opts.wait,
WaitTimeout: timeout,
})
}
================================================
FILE: cmd/compose/stats.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/container"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
)
type statsOptions struct {
ProjectOptions *ProjectOptions
all bool
format string
noStream bool
noTrunc bool
}
func statsCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
opts := statsOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "stats [OPTIONS] [SERVICE]",
Short: "Display a live stream of container(s) resource usage statistics",
Args: cobra.MaximumNArgs(1),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStats(ctx, dockerCli, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
flags.StringVar(&opts.format, "format", "", `Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates`)
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
return cmd
}
func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error {
name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
f := client.Filters{}
f.Add("label", fmt.Sprintf("%s=%s", api.ProjectLabel, name))
if len(service) > 0 {
f.Add("label", fmt.Sprintf("%s=%s", api.ServiceLabel, service[0]))
}
return container.RunStats(ctx, dockerCli, &container.StatsOptions{
All: opts.all,
NoStream: opts.noStream,
NoTrunc: opts.noTrunc,
Format: opts.format,
Filters: f,
})
}
================================================
FILE: cmd/compose/stop.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type stopOptions struct {
*ProjectOptions
timeChanged bool
timeout int
}
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := stopOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "stop [OPTIONS] [SERVICE...]",
Short: "Stop services",
PreRun: func(cmd *cobra.Command, args []string) {
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStop(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
return cmd
}
func runStop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts stopOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Stop(ctx, name, api.StopOptions{
Timeout: timeout,
Services: services,
Project: project,
})
}
================================================
FILE: cmd/compose/top.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"sort"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type topOptions struct {
*ProjectOptions
}
func topCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := topOptions{
ProjectOptions: p,
}
topCmd := &cobra.Command{
Use: "top [SERVICES...]",
Short: "Display the running processes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runTop(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return topCmd
}
type (
topHeader map[string]int // maps a proc title to its output index
topEntries map[string]string
)
func runTop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
containers, err := backend.Top(ctx, projectName, services)
if err != nil {
return err
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
header, entries := collectTop(containers)
return topPrint(dockerCli.Out(), header, entries)
}
func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
// map column name to its header (should keep working if backend.Top returns
// varying columns for different containers)
header := topHeader{"SERVICE": 0, "#": 1}
// assume one process per container and grow if needed
entries := make([]topEntries, 0, len(containers))
for _, container := range containers {
for _, proc := range container.Processes {
entry := topEntries{
"SERVICE": container.Service,
"#": container.Replica,
}
for i, title := range container.Titles {
if _, exists := header[title]; !exists {
header[title] = len(header)
}
entry[title] = proc[i]
}
entries = append(entries, entry)
}
}
// ensure CMD is the right-most column
if pos, ok := header["CMD"]; ok {
maxPos := pos
for h, i := range header {
if i > maxPos {
maxPos = i
}
if i > pos {
header[h] = i - 1
}
}
header["CMD"] = maxPos
}
return header, entries
}
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error {
if len(rows) == 0 {
return nil
}
w := tabwriter.NewWriter(out, 4, 1, 2, ' ', 0)
// write headers in the order we've encountered them
h := make([]string, len(headers))
for title, index := range headers {
h[index] = title
}
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))
for _, row := range rows {
// write proc data in header order
r := make([]string, len(headers))
for title, index := range headers {
if v, ok := row[title]; ok {
r[index] = v
} else {
r[index] = "-"
}
}
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
}
return w.Flush()
}
================================================
FILE: cmd/compose/top_test.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v5/pkg/api"
)
var topTestCases = []struct {
name string
titles []string
procs [][]string
header topHeader
entries []topEntries
output string
}{
{
name: "noprocs",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{},
header: topHeader{"SERVICE": 0, "#": 1},
entries: []topEntries{},
output: "",
},
{
name: "simple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 /entrypoint
`),
},
{
name: "noppid",
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
},
entries: []topEntries{
{
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID C STIME TTY TIME CMD
noppid 1 root 1 0 12:00 ? 00:00:02 /entrypoint
`),
},
{
name: "extra-hdr",
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"GID": 3,
"PID": 4,
"PPID": 5,
"C": 6,
"STIME": 7,
"TTY": 8,
"TIME": 9,
"CMD": 10,
},
entries: []topEntries{
{
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID GID PID PPID C STIME TTY TIME CMD
extra-hdr 1 root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
`),
},
{
name: "multiple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
},
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
multiple 1 root 1 1 0 12:00 ? 00:00:04 /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 sleep infinity
`),
},
}
// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Compose).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()
all := []api.ContainerProcSummary{}
for _, tc := range topTestCases {
summary := api.ContainerProcSummary{
Name: "not used",
Titles: tc.titles,
Processes: tc.procs,
Service: tc.name,
Replica: "1",
}
all = append(all, summary)
t.Run(tc.name, func(t *testing.T) {
header, entries := collectTop([]api.ContainerProcSummary{summary})
assert.Equal(t, tc.header, header)
assert.Equal(t, tc.entries, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, tc.output, buf.String())
})
}
t.Run("all", func(t *testing.T) {
header, entries := collectTop(all)
assert.Equal(t, topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"GID": 9,
"CMD": 10,
}, header)
assert.Equal(t, []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
}, {
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
}, {
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
}, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, trim(`
SERVICE # UID PID PPID C STIME TTY TIME GID CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 - /entrypoint
noppid 1 root 1 - 0 12:00 ? 00:00:02 - /entrypoint
extra-hdr 1 root 1 1 0 12:00 ? 00:00:03 1 /entrypoint
multiple 1 root 1 1 0 12:00 ? 00:00:04 - /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 - sleep infinity
`), buf.String())
})
}
func trim(s string) string {
var out bytes.Buffer
for line := range strings.SplitSeq(strings.TrimSpace(s), "\n") {
out.WriteString(strings.TrimSpace(line))
out.WriteRune('\n')
}
return out.String()
}
================================================
FILE: cmd/compose/up.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
// composeOptions hold options common to `up` and `run` to run compose project
type composeOptions struct {
*ProjectOptions
}
type upOptions struct {
*composeOptions
Detach bool
noStart bool
noDeps bool
cascadeStop bool
cascadeFail bool
exitCodeFrom string
noColor bool
noPrefix bool
attachDependencies bool
attach []string
noAttach []string
timestamp bool
wait bool
waitTimeout int
watch bool
navigationMenu bool
navigationMenuChanged bool
}
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
if opts.noDeps {
var err error
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
if opts.exitCodeFrom != "" {
_, err := project.GetService(opts.exitCodeFrom)
if err != nil {
return nil, err
}
}
return project, nil
}
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) {
if !dockerCli.Out().IsTerminal() {
opts.navigationMenu = false
return
}
// If --menu flag was not set
if !opts.navigationMenuChanged {
if envVar, ok := os.LookupEnv(ComposeMenu); ok {
opts.navigationMenu = utils.StringToBool(envVar)
return
}
// ...and COMPOSE_MENU env var is not defined we want the default value to be true
opts.navigationMenu = true
}
}
func (opts upOptions) OnExit() api.Cascade {
switch {
case opts.cascadeStop:
return api.CascadeStop
case opts.cascadeFail:
return api.CascadeFail
default:
return api.CascadeIgnore
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
up := upOptions{}
create := createOptions{}
build := buildOptions{ProjectOptions: p}
upCmd := &cobra.Command{
Use: "up [OPTIONS] [SERVICE...]",
Short: "Create and start containers",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
create.pullChanged = cmd.Flags().Changed("pull")
create.timeChanged = cmd.Flags().Changed("timeout")
up.navigationMenuChanged = cmd.Flags().Changed("menu")
if !cmd.Flags().Changed("remove-orphans") {
create.removeOrphans = utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
}
return validateFlags(&up, &create)
}),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
create.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
if create.ignoreOrphans && create.removeOrphans {
return fmt.Errorf("cannot combine %s and --remove-orphans", ComposeIgnoreOrphans)
}
if len(up.attach) != 0 && up.attachDependencies {
return errors.New("cannot combine --attach and --attach-dependencies")
}
up.validateNavigationMenu(dockerCli)
if !p.All && len(project.Services) == 0 {
return fmt.Errorf("no service selected")
}
return runUp(ctx, dockerCli, backendOptions, create, up, build, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := upCmd.Flags()
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
flags.BoolVar(&create.Build, "build", false, "Build images before starting containers")
flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy")
flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
flags.StringArrayVar(&create.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output")
flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs")
flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed")
flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them")
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
flags.BoolVar(&up.cascadeFail, "abort-on-container-failure", false, "Stops all containers if any container exited with failure. Incompatible with -d")
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running")
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps")
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services")
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers")
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&build.quiet, "quiet-build", false, "Suppress the build output")
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
flags.BoolVarP(&create.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return upCmd
}
//nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error {
if up.waitTimeout < 0 {
return fmt.Errorf("--wait-timeout must be a non-negative integer")
}
if up.exitCodeFrom != "" && !up.cascadeFail {
up.cascadeStop = true
}
if up.cascadeStop && up.cascadeFail {
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
}
if up.wait {
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
}
up.Detach = true
}
if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
if up.Detach && (up.attachDependencies || up.cascadeStop || up.cascadeFail || len(up.attach) > 0 || up.watch) {
if up.wait {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
} else {
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
}
}
if create.noInherit && create.noRecreate {
return fmt.Errorf("--no-recreate and --renew-anon-volumes are incompatible")
}
if create.forceRecreate && create.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
}
if create.recreateDeps && create.noRecreate {
return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
}
if create.noBuild && up.watch {
return fmt.Errorf("--no-build and --watch are incompatible")
}
return nil
}
//nolint:gocyclo
func runUp(
ctx context.Context,
dockerCli command.Cli,
backendOptions *BackendOptions,
createOptions createOptions,
upOptions upOptions,
buildOptions buildOptions,
project *types.Project,
services []string,
) error {
if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil {
return err
}
err := createOptions.Apply(project)
if err != nil {
return err
}
project, err = upOptions.apply(project, services)
if err != nil {
return err
}
var build *api.BuildOptions
if !createOptions.noBuild {
if createOptions.quietPull {
buildOptions.Progress = string(xprogress.QuietMode)
}
// BuildOptions here is nested inside CreateOptions, so
// no service list is passed, it will implicitly pick all
// services being created, which includes any explicitly
// specified via "services" arg here as well as deps
bo, err := buildOptions.toAPIBuildOptions(nil)
if err != nil {
return err
}
bo.Services = project.ServiceNames()
bo.Deps = !upOptions.noDeps
build = &bo
}
create := api.CreateOptions{
Build: build,
Services: services,
RemoveOrphans: createOptions.removeOrphans,
IgnoreOrphans: createOptions.ignoreOrphans,
Recreate: createOptions.recreateStrategy(),
RecreateDependencies: createOptions.dependenciesRecreateStrategy(),
Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull,
}
if createOptions.AssumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
if upOptions.noStart {
return backend.Create(ctx, project, create)
}
var consumer api.LogConsumer
var attach []string
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
var attachSet utils.Set[string]
if len(upOptions.attach) != 0 {
// services are passed explicitly with --attach, verify they're valid and then use them as-is
attachSet = utils.NewSet(upOptions.attach...)
unexpectedSvcs := attachSet.Diff(utils.NewSet(project.ServiceNames()...))
if len(unexpectedSvcs) != 0 {
return fmt.Errorf("cannot attach to services not included in up: %s", strings.Join(unexpectedSvcs.Elements(), ", "))
}
} else {
// mark services being launched (and potentially their deps) for attach
// if they didn't opt-out via Compose YAML
attachSet = utils.NewSet[string]()
var dependencyOpt types.DependencyOption = types.IgnoreDependencies
if upOptions.attachDependencies {
dependencyOpt = types.IncludeDependencies
}
if err := project.ForEachService(services, func(serviceName string, s *types.ServiceConfig) error {
if s.Attach == nil || *s.Attach {
attachSet.Add(serviceName)
}
return nil
}, dependencyOpt); err != nil {
return err
}
}
// filter out any services that have been explicitly marked for ignore with `--no-attach`
attachSet.RemoveAll(upOptions.noAttach...)
attach = attachSet.Elements()
}
var timeout time.Duration
if upOptions.waitTimeout > 0 {
timeout = time.Duration(upOptions.waitTimeout) * time.Second
}
return backend.Up(ctx, project, api.UpOptions{
Create: create,
Start: api.StartOptions{
Project: project,
Attach: consumer,
AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom,
OnExit: upOptions.OnExit(),
Wait: upOptions.wait,
WaitTimeout: timeout,
Watch: upOptions.watch,
Services: services,
NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(),
},
})
}
func setServiceScale(project *types.Project, name string, replicas int) error {
service, err := project.GetService(name)
if err != nil {
return err
}
service.SetScale(replicas)
project.Services[name] = service
return nil
}
================================================
FILE: cmd/compose/up_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestApplyScaleOpt(t *testing.T) {
p := types.Project{
Services: types.Services{
"foo": {
Name: "foo",
},
"bar": {
Name: "bar",
Deploy: &types.DeployConfig{
Mode: "test",
},
},
},
}
err := applyScaleOpts(&p, []string{"foo=2", "bar=3"})
assert.NilError(t, err)
foo, err := p.GetService("foo")
assert.NilError(t, err)
assert.Equal(t, *foo.Scale, 2)
bar, err := p.GetService("bar")
assert.NilError(t, err)
assert.Equal(t, *bar.Scale, 3)
assert.Equal(t, *bar.Deploy.Replicas, 3)
}
func TestUpOptions_OnExit(t *testing.T) {
tests := []struct {
name string
args upOptions
want api.Cascade
}{
{
name: "no cascade",
args: upOptions{},
want: api.CascadeIgnore,
},
{
name: "cascade stop",
args: upOptions{cascadeStop: true},
want: api.CascadeStop,
},
{
name: "cascade fail",
args: upOptions{cascadeFail: true},
want: api.CascadeFail,
},
{
name: "both set - stop takes precedence",
args: upOptions{
cascadeStop: true,
cascadeFail: true,
},
want: api.CascadeStop,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.args.OnExit()
assert.Equal(t, got, tt.want)
})
}
}
================================================
FILE: cmd/compose/version.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"strings"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal"
)
type versionOptions struct {
format string
short bool
}
func versionCommand(dockerCli command.Cli) *cobra.Command {
opts := versionOptions{}
cmd := &cobra.Command{
Use: "version [OPTIONS]",
Short: "Show the Docker Compose version information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
runVersion(opts, dockerCli)
return nil
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// overwrite parent PersistentPreRunE to avoid trying to load
// compose file on version command if COMPOSE_FILE is set
return nil
},
}
// define flags for backward compatibility with com.docker.cli
flags := cmd.Flags()
flags.StringVarP(&opts.format, "format", "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number")
return cmd
}
func runVersion(opts versionOptions, dockerCli command.Cli) {
if opts.short {
_, _ = fmt.Fprintln(dockerCli.Out(), strings.TrimPrefix(internal.Version, "v"))
return
}
if opts.format == formatter.JSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "{\"version\":%q}\n", internal.Version)
return
}
_, _ = fmt.Fprintln(dockerCli.Out(), "Docker Compose version", internal.Version)
}
================================================
FILE: cmd/compose/version_test.go
================================================
/*
Copyright 2025 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"testing"
"github.com/docker/cli/cli/streams"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestVersionCommand(t *testing.T) {
originalVersion := internal.Version
defer func() {
internal.Version = originalVersion
}()
internal.Version = "v9.9.9-test"
tests := []struct {
name string
args []string
want string
}{
{
name: "default",
args: []string{},
want: "Docker Compose version v9.9.9-test\n",
},
{
name: "short flag",
args: []string{"--short"},
want: "9.9.9-test\n",
},
{
name: "json flag",
args: []string{"--format", "json"},
want: `{"version":"v9.9.9-test"}` + "\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
cmd := versionCommand(cli)
cmd.SetArgs(test.args)
err := cmd.Execute()
assert.NilError(t, err)
assert.Equal(t, test.want, buf.String())
})
}
}
================================================
FILE: cmd/compose/viz.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type vizOptions struct {
*ProjectOptions
includeNetworks bool
includePorts bool
includeImageName bool
indentationStr string
}
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := vizOptions{
ProjectOptions: p,
}
var indentationSize int
var useSpaces bool
cmd := &cobra.Command{
Use: "viz [OPTIONS]",
Short: "EXPERIMENTAL - Generate a graphviz graph from your compose file",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
var err error
opts.indentationStr, err = preferredIndentationStr(indentationSize, useSpaces)
return err
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runViz(ctx, dockerCli, backendOptions, &opts)
}),
}
cmd.Flags().BoolVar(&opts.includePorts, "ports", false, "Include service's exposed ports in output graph")
cmd.Flags().BoolVar(&opts.includeNetworks, "networks", false, "Include service's attached networks in output graph")
cmd.Flags().BoolVar(&opts.includeImageName, "image", false, "Include service's image name in output graph")
cmd.Flags().IntVar(&indentationSize, "indentation-size", 1, "Number of tabs or spaces to use for indentation")
cmd.Flags().BoolVar(&useSpaces, "spaces", false, "If given, space character ' ' will be used to indent,\notherwise tab character '\\t' will be used")
return cmd
}
func runViz(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *vizOptions) error {
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}
// build graph
graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
IncludeNetworks: opts.includeNetworks,
IncludePorts: opts.includePorts,
IncludeImageName: opts.includeImageName,
Indentation: opts.indentationStr,
})
fmt.Println(graphStr)
return nil
}
// preferredIndentationStr returns a single string given the indentation preference
func preferredIndentationStr(size int, useSpace bool) (string, error) {
if size < 0 {
return "", fmt.Errorf("invalid indentation size: %d", size)
}
indentationStr := "\t"
if useSpace {
indentationStr = " "
}
return strings.Repeat(indentationStr, size), nil
}
================================================
FILE: cmd/compose/viz_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPreferredIndentationStr(t *testing.T) {
type args struct {
size int
useSpace bool
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "should return '\\t\\t'",
args: args{
size: 2,
useSpace: false,
},
want: "\t\t",
wantErr: false,
},
{
name: "should return ' '",
args: args{
size: 4,
useSpace: true,
},
want: " ",
wantErr: false,
},
{
name: "should return ''",
args: args{
size: 0,
useSpace: false,
},
want: "",
wantErr: false,
},
{
name: "should return ''",
args: args{
size: 0,
useSpace: true,
},
want: "",
wantErr: false,
},
{
name: "should throw error because indentation size < 0",
args: args{
size: -1,
useSpace: false,
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace)
if tt.wantErr {
require.Errorf(t, err, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
} else {
require.NoError(t, err)
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
}
})
}
}
================================================
FILE: cmd/compose/volumes.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type volumesOptions struct {
*ProjectOptions
Quiet bool
Format string
}
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := volumesOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "volumes [OPTIONS] [SERVICE...]",
Short: "List volumes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runVol(ctx, dockerCli, backendOptions, args, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
return cmd
}
func runVol(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, options volumesOptions) error {
project, name, err := options.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
if project != nil {
names := project.ServiceNames()
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{
Services: services,
})
if err != nil {
return err
}
if options.Quiet {
for _, v := range volumes {
_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
}
return nil
}
volumeCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
}
return formatter.VolumeWrite(volumeCtx, volumes)
}
================================================
FILE: cmd/compose/wait.go
================================================
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type waitOptions struct {
*ProjectOptions
services []string
downProject bool
}
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
var statusCode int64
var err error
cmd := &cobra.Command{
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
Short: "Block until containers of all (or specified) services stop.",
Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services
statusCode, err = runWait(ctx, dockerCli, backendOptions, &opts)
return err
}),
PostRun: func(cmd *cobra.Command, args []string) {
os.Exit(int(statusCode))
},
}
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
return cmd
}
func runWait(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return 0, err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return 0, err
}
return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,
})
}
================================================
FILE: cmd/compose/watch.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal/locker"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type watchOptions struct {
*ProjectOptions
prune bool
noUp bool
}
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
watchOpts := watchOptions{
ProjectOptions: p,
}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "watch [SERVICE...]",
Short: "Watch build context for service and rebuild/refresh containers when files are updated",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Parent().Name() == "alpha" {
logrus.Warn("watch command is now available as a top level command")
}
return runWatch(ctx, dockerCli, backendOptions, watchOpts, buildOpts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output")
cmd.Flags().BoolVar(&watchOpts.prune, "prune", true, "Prune dangling images on rebuild")
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
return cmd
}
func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
if err := applyPlatforms(project, true); err != nil {
return err
}
build, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
// validation done -- ensure we have the lockfile for this project before doing work
l, err := locker.NewPidfile(project.Name)
if err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
}
if err := l.Lock(); err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
}
if !watchOpts.noUp {
for index, service := range project.Services {
if service.Build != nil && service.Develop != nil {
service.PullPolicy = types.PullPolicyBuild
}
project.Services[index] = service
}
upOpts := api.UpOptions{
Create: api.CreateOptions{
Build: &build,
Services: services,
RemoveOrphans: false,
Recreate: api.RecreateDiverged,
RecreateDependencies: api.RecreateNever,
Inherit: true,
QuietPull: buildOpts.quiet,
},
Start: api.StartOptions{
Project: project,
Attach: nil,
Services: services,
},
}
if err := backend.Up(ctx, project, upOpts); err != nil {
return err
}
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
return backend.Watch(ctx, project, api.WatchOptions{
Build: &build,
LogTo: consumer,
Prune: watchOpts.prune,
Services: services,
})
}
================================================
FILE: cmd/display/colors.go
================================================
/*
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 display
import (
"github.com/morikuni/aec"
)
type colorFunc func(string) string
var (
nocolor colorFunc = func(s string) string {
return s
}
DoneColor colorFunc = aec.BlueF.Apply
TimerColor colorFunc = aec.BlueF.Apply
CountColor colorFunc = aec.YellowF.Apply
WarningColor colorFunc = aec.YellowF.With(aec.Bold).Apply
SuccessColor colorFunc = aec.GreenF.Apply
ErrorColor colorFunc = aec.RedF.With(aec.Bold).Apply
PrefixColor colorFunc = aec.CyanF.Apply
)
func NoColor() {
DoneColor = nocolor
TimerColor = nocolor
CountColor = nocolor
WarningColor = nocolor
SuccessColor = nocolor
ErrorColor = nocolor
PrefixColor = nocolor
}
================================================
FILE: cmd/display/dryrun.go
================================================
/*
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 display
const (
DRYRUN_PREFIX = " DRY-RUN MODE - "
)
================================================
FILE: cmd/display/json.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package display
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/docker/compose/v5/pkg/api"
)
func JSON(out io.Writer) api.EventProcessor {
return &jsonWriter{
out: out,
}
}
type jsonWriter struct {
out io.Writer
dryRun bool
}
type jsonMessage struct {
DryRun bool `json:"dry-run,omitempty"`
Tail bool `json:"tail,omitempty"`
ID string `json:"id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Status string `json:"status,omitempty"`
Text string `json:"text,omitempty"`
Details string `json:"details,omitempty"`
Current int64 `json:"current,omitempty"`
Total int64 `json:"total,omitempty"`
Percent int `json:"percent,omitempty"`
}
func (p *jsonWriter) Start(ctx context.Context, operation string) {
}
func (p *jsonWriter) Event(e api.Resource) {
message := &jsonMessage{
DryRun: p.dryRun,
Tail: false,
ID: e.ID,
Status: e.StatusText(),
Text: e.Text,
Details: e.Details,
ParentID: e.ParentID,
Current: e.Current,
Total: e.Total,
Percent: e.Percent,
}
marshal, err := json.Marshal(message)
if err == nil {
_, _ = fmt.Fprintln(p.out, string(marshal))
}
}
func (p *jsonWriter) On(events ...api.Resource) {
for _, e := range events {
p.Event(e)
}
}
func (p *jsonWriter) Done(_ string, _ bool) {
}
================================================
FILE: cmd/display/json_test.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package display
import (
"bytes"
"encoding/json"
"testing"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestJsonWriter_Event(t *testing.T) {
var out bytes.Buffer
w := &jsonWriter{
out: &out,
dryRun: true,
}
event := api.Resource{
ID: "service1",
ParentID: "project",
Status: api.Working,
Text: api.StatusCreating,
Current: 50,
Total: 100,
Percent: 50,
}
w.Event(event)
var actual jsonMessage
err := json.Unmarshal(out.Bytes(), &actual)
assert.NilError(t, err)
expected := jsonMessage{
DryRun: true,
ID: event.ID,
ParentID: event.ParentID,
Text: api.StatusCreating,
Status: "Working",
Current: event.Current,
Total: event.Total,
Percent: event.Percent,
}
assert.DeepEqual(t, expected, actual)
}
================================================
FILE: cmd/display/mode.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package display
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
var Mode = ModeAuto
const (
// ModeAuto detect console capabilities
ModeAuto = "auto"
// ModeTTY use terminal capability for advanced rendering
ModeTTY = "tty"
// ModePlain dump raw events to output
ModePlain = "plain"
// ModeQuiet don't display events
ModeQuiet = "quiet"
// ModeJSON outputs a machine-readable JSON stream
ModeJSON = "json"
)
================================================
FILE: cmd/display/plain.go
================================================
/*
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 display
import (
"context"
"fmt"
"io"
"github.com/docker/compose/v5/pkg/api"
)
func Plain(out io.Writer) api.EventProcessor {
return &plainWriter{
out: out,
}
}
type plainWriter struct {
out io.Writer
dryRun bool
}
func (p *plainWriter) Start(ctx context.Context, operation string) {
}
func (p *plainWriter) Event(e api.Resource) {
prefix := ""
if p.dryRun {
prefix = DRYRUN_PREFIX
}
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
}
func (p *plainWriter) On(events ...api.Resource) {
for _, e := range events {
p.Event(e)
}
}
func (p *plainWriter) Done(_ string, _ bool) {
}
================================================
FILE: cmd/display/quiet.go
================================================
/*
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 display
import (
"context"
"github.com/docker/compose/v5/pkg/api"
)
func Quiet() api.EventProcessor {
return &quiet{}
}
type quiet struct{}
func (q *quiet) Start(_ context.Context, _ string) {
}
func (q *quiet) Done(_ string, _ bool) {
}
func (q *quiet) On(_ ...api.Resource) {
}
================================================
FILE: cmd/display/spinner.go
================================================
/*
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 display
import (
"runtime"
"time"
)
type Spinner struct {
time time.Time
index int
chars []string
stop bool
done string
}
func NewSpinner() *Spinner {
chars := []string{
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
}
done := "⠿"
if runtime.GOOS == "windows" {
chars = []string{"-"}
done = "-"
}
return &Spinner{
index: 0,
time: time.Now(),
chars: chars,
done: done,
}
}
func (s *Spinner) String() string {
if s.stop {
return s.done
}
d := time.Since(s.time)
if d.Milliseconds() > 100 {
s.index = (s.index + 1) % len(s.chars)
}
return s.chars[s.index]
}
func (s *Spinner) Stop() {
s.stop = true
}
func (s *Spinner) Restart() {
s.stop = false
}
================================================
FILE: cmd/display/tty.go
================================================
/*
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 display
import (
"context"
"fmt"
"io"
"iter"
"slices"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/buger/goterm"
"github.com/docker/go-units"
"github.com/morikuni/aec"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
// Full creates an EventProcessor that render advanced UI within a terminal.
// On Start, TUI lists task with a progress timer
func Full(out io.Writer, info io.Writer, detached bool) api.EventProcessor {
return &ttyWriter{
out: out,
info: info,
tasks: map[string]*task{},
done: make(chan bool),
mtx: &sync.Mutex{},
detached: detached,
}
}
type ttyWriter struct {
out io.Writer
ids []string // tasks ids ordered as first event appeared
tasks map[string]*task
repeated bool
numLines int
done chan bool
mtx *sync.Mutex
dryRun bool // FIXME(ndeloof) (re)implement support for dry-run
operation string
ticker *time.Ticker
suspended bool
info io.Writer
detached bool
}
type task struct {
ID string
parent string // the resource this task receives updates from - other parents will be ignored
parents utils.Set[string] // all resources to depend on this task
startTime time.Time
endTime time.Time
text string
details string
status api.EventStatus
current int64
percent int
total int64
spinner *Spinner
}
func newTask(e api.Resource) task {
t := task{
ID: e.ID,
parents: utils.NewSet[string](),
startTime: time.Now(),
text: e.Text,
details: e.Details,
status: e.Status,
current: e.Current,
percent: e.Percent,
total: e.Total,
spinner: NewSpinner(),
}
if e.ParentID != "" {
t.parent = e.ParentID
t.parents.Add(e.ParentID)
}
if e.Status == api.Done || e.Status == api.Error {
t.stop()
}
return t
}
// update adjusts task state based on last received event
func (t *task) update(e api.Resource) {
if e.ParentID != "" {
t.parents.Add(e.ParentID)
// we may receive same event from distinct parents (typically: images sharing layers)
// to avoid status to flicker, only accept updates from our first declared parent
if t.parent != e.ParentID {
return
}
}
// update task based on received event
switch e.Status {
case api.Done, api.Error, api.Warning:
if t.status != e.Status {
t.stop()
}
case api.Working:
t.hasMore()
}
t.status = e.Status
t.text = e.Text
t.details = e.Details
// progress can only go up
if e.Total > t.total {
t.total = e.Total
}
if e.Current > t.current {
t.current = e.Current
}
if e.Percent > t.percent {
t.percent = e.Percent
}
}
func (t *task) stop() {
t.endTime = time.Now()
t.spinner.Stop()
}
func (t *task) hasMore() {
t.spinner.Restart()
}
func (t *task) Completed() bool {
switch t.status {
case api.Done, api.Error, api.Warning:
return true
default:
return false
}
}
func (w *ttyWriter) Start(ctx context.Context, operation string) {
w.ticker = time.NewTicker(100 * time.Millisecond)
w.operation = operation
go func() {
for {
select {
case <-ctx.Done():
// interrupted
w.ticker.Stop()
return
case <-w.done:
return
case <-w.ticker.C:
w.print()
}
}
}()
}
func (w *ttyWriter) Done(operation string, success bool) {
w.print()
w.done <- true
w.mtx.Lock()
defer w.mtx.Unlock()
if w.ticker != nil {
w.ticker.Stop()
}
w.operation = ""
}
func (w *ttyWriter) On(events ...api.Resource) {
w.mtx.Lock()
defer w.mtx.Unlock()
for _, e := range events {
if e.ID == "Compose" {
_, _ = fmt.Fprintln(w.info, ErrorColor(e.Details))
continue
}
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) && !w.detached {
// skip those events to avoid mix with container logs
continue
}
w.event(e)
}
}
func (w *ttyWriter) event(e api.Resource) {
// Suspend print while a build is in progress, to avoid collision with buildkit Display
if w.ticker != nil {
if e.Text == api.StatusBuilding {
w.ticker.Stop()
w.suspended = true
} else if w.suspended {
w.ticker.Reset(100 * time.Millisecond)
w.suspended = false
}
}
if last, ok := w.tasks[e.ID]; ok {
last.update(e)
} else {
t := newTask(e)
w.tasks[e.ID] = &t
w.ids = append(w.ids, e.ID)
}
w.printEvent(e)
}
func (w *ttyWriter) printEvent(e api.Resource) {
if w.operation != "" {
// event will be displayed by progress UI on ticker's ticks
return
}
var color colorFunc
switch e.Status {
case api.Working:
color = SuccessColor
case api.Done:
color = SuccessColor
case api.Warning:
color = WarningColor
case api.Error:
color = ErrorColor
}
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
}
func (w *ttyWriter) parentTasks() iter.Seq[*task] {
return func(yield func(*task) bool) {
for _, id := range w.ids { // iterate on ids to enforce a consistent order
t := w.tasks[id]
if len(t.parents) == 0 {
yield(t)
}
}
}
}
func (w *ttyWriter) childrenTasks(parent string) iter.Seq[*task] {
return func(yield func(*task) bool) {
for _, id := range w.ids { // iterate on ids to enforce a consistent order
t := w.tasks[id]
if t.parents.Has(parent) {
yield(t)
}
}
}
}
// lineData holds pre-computed formatting for a task line
type lineData struct {
spinner string // rendered spinner with color
prefix string // dry-run prefix if any
taskID string // possibly abbreviated
progress string // progress bar and size info
status string // rendered status with color
details string // possibly abbreviated
timer string // rendered timer with color
statusPad int // padding before status to align
timerPad int // padding before timer to align
statusColor colorFunc
}
func (w *ttyWriter) print() {
terminalWidth := goterm.Width()
terminalHeight := goterm.Height()
if terminalWidth <= 0 {
terminalWidth = 80
}
if terminalHeight <= 0 {
terminalHeight = 24
}
w.printWithDimensions(terminalWidth, terminalHeight)
}
func (w *ttyWriter) printWithDimensions(terminalWidth, terminalHeight int) {
w.mtx.Lock()
defer w.mtx.Unlock()
if len(w.tasks) == 0 {
return
}
up := w.numLines + 1
if !w.repeated {
up--
w.repeated = true
}
b := aec.NewBuilder(
aec.Hide, // Hide the cursor while we are printing
aec.Up(uint(up)),
aec.Column(0),
)
_, _ = fmt.Fprint(w.out, b.ANSI)
defer func() {
_, _ = fmt.Fprint(w.out, aec.Show)
}()
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
_, _ = fmt.Fprintln(w.out, firstLine)
// Collect parent tasks in original order
allTasks := slices.Collect(w.parentTasks())
// Available lines: terminal height - 2 (header line + potential "more" line)
maxLines := max(terminalHeight-2, 1)
showMore := len(allTasks) > maxLines
tasksToShow := allTasks
if showMore {
tasksToShow = allTasks[:maxLines-1] // Reserve one line for "more" message
}
// collect line data and compute timerLen
lines := make([]lineData, len(tasksToShow))
var timerLen int
for i, t := range tasksToShow {
lines[i] = w.prepareLineData(t)
if len(lines[i].timer) > timerLen {
timerLen = len(lines[i].timer)
}
}
// shorten details/taskID to fit terminal width
w.adjustLineWidth(lines, timerLen, terminalWidth)
// compute padding
w.applyPadding(lines, terminalWidth, timerLen)
// Render lines
numLines := 0
for _, l := range lines {
_, _ = fmt.Fprint(w.out, lineText(l))
numLines++
}
if showMore {
moreCount := len(allTasks) - len(tasksToShow)
moreText := fmt.Sprintf(" ... %d more", moreCount)
pad := max(terminalWidth-len(moreText), 0)
_, _ = fmt.Fprintf(w.out, "%s%s\n", moreText, strings.Repeat(" ", pad))
numLines++
}
// Clear any remaining lines from previous render
for i := numLines; i < w.numLines; i++ {
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
numLines++
}
w.numLines = numLines
}
func (w *ttyWriter) applyPadding(lines []lineData, terminalWidth int, timerLen int) {
var maxBeforeStatus int
for i := range lines {
l := &lines[i]
// Width before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
if beforeStatus > maxBeforeStatus {
maxBeforeStatus = beforeStatus
}
}
for i, l := range lines {
// Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
// statusPad aligns status; lineText adds 1 more space after statusPad
l.statusPad = maxBeforeStatus - beforeStatus
// Format: beforeStatus + statusPad + space(1) + status
lineLen := beforeStatus + l.statusPad + 1 + utf8.RuneCountInString(l.status)
if l.details != "" {
lineLen += 1 + utf8.RuneCountInString(l.details)
}
l.timerPad = max(terminalWidth-lineLen-timerLen, 1)
lines[i] = l
}
}
func (w *ttyWriter) adjustLineWidth(lines []lineData, timerLen int, terminalWidth int) {
const minIDLen = 10
maxStatusLen := maxStatusLength(lines)
// Iteratively truncate until all lines fit
for range 100 { // safety limit
maxBeforeStatus := maxBeforeStatusWidth(lines)
overflow := computeOverflow(lines, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth)
if overflow <= 0 {
break
}
// First try to truncate details, then taskID
if !truncateDetails(lines, overflow) && !truncateLongestTaskID(lines, overflow, minIDLen) {
break // Can't truncate further
}
}
}
// maxStatusLength returns the maximum status text length across all lines.
func maxStatusLength(lines []lineData) int {
var maxLen int
for i := range lines {
if len(lines[i].status) > maxLen {
maxLen = len(lines[i].status)
}
}
return maxLen
}
// maxBeforeStatusWidth computes the maximum width before statusPad across all lines.
// This is: space(1) + spinner(1) + prefix + space(1) + taskID + progress
func maxBeforeStatusWidth(lines []lineData) int {
var maxWidth int
for i := range lines {
l := &lines[i]
width := 3 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
if width > maxWidth {
maxWidth = width
}
}
return maxWidth
}
// computeOverflow calculates how many characters the widest line exceeds the terminal width.
// Returns 0 or negative if all lines fit.
func computeOverflow(lines []lineData, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth int) int {
var maxOverflow int
for i := range lines {
l := &lines[i]
detailsLen := len(l.details)
if detailsLen > 0 {
detailsLen++ // space before details
}
// Line width: maxBeforeStatus + space(1) + status + details + minTimerPad(1) + timer
lineWidth := maxBeforeStatus + 1 + maxStatusLen + detailsLen + 1 + timerLen
overflow := lineWidth - terminalWidth
if overflow > maxOverflow {
maxOverflow = overflow
}
}
return maxOverflow
}
// truncateDetails tries to truncate the first line's details to reduce overflow.
// Returns true if any truncation was performed.
func truncateDetails(lines []lineData, overflow int) bool {
for i := range lines {
l := &lines[i]
if len(l.details) > 3 {
reduction := min(overflow, len(l.details)-3)
l.details = l.details[:len(l.details)-reduction-3] + "..."
return true
} else if l.details != "" {
l.details = ""
return true
}
}
return false
}
// truncateLongestTaskID truncates the longest taskID to reduce overflow.
// Returns true if truncation was performed.
func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
longestIdx := -1
longestLen := minIDLen
for i := range lines {
if len(lines[i].taskID) > longestLen {
longestLen = len(lines[i].taskID)
longestIdx = i
}
}
if longestIdx < 0 {
return false
}
l := &lines[longestIdx]
reduction := overflow + 3 // account for "..."
newLen := max(len(l.taskID)-reduction, minIDLen-3)
if newLen > 0 {
l.taskID = l.taskID[:newLen] + "..."
}
return true
}
func (w *ttyWriter) prepareLineData(t *task) lineData {
endTime := time.Now()
if t.status != api.Working {
endTime = t.startTime
if (t.endTime != time.Time{}) {
endTime = t.endTime
}
}
prefix := ""
if w.dryRun {
prefix = PrefixColor(DRYRUN_PREFIX)
}
elapsed := endTime.Sub(t.startTime).Seconds()
var (
hideDetails bool
total int64
current int64
completion []string
)
// only show the aggregated progress while the root operation is in-progress
if t.status == api.Working {
for child := range w.childrenTasks(t.ID) {
if child.status == api.Working && child.total == 0 {
hideDetails = true
}
total += child.total
current += child.current
r := len(percentChars) - 1
p := min(child.percent, 100)
completion = append(completion, percentChars[r*p/100])
}
}
if total == 0 {
hideDetails = true
}
var progress string
if len(completion) > 0 {
progress = " [" + SuccessColor(strings.Join(completion, "")) + "]"
if !hideDetails {
progress += fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
}
}
return lineData{
spinner: spinner(t),
prefix: prefix,
taskID: t.ID,
progress: progress,
status: t.text,
statusColor: colorFn(t.status),
details: t.details,
timer: fmt.Sprintf("%.1fs", elapsed),
}
}
func lineText(l lineData) string {
var sb strings.Builder
sb.WriteString(" ")
sb.WriteString(l.spinner)
sb.WriteString(l.prefix)
sb.WriteString(" ")
sb.WriteString(l.taskID)
sb.WriteString(l.progress)
sb.WriteString(strings.Repeat(" ", l.statusPad))
sb.WriteString(" ")
sb.WriteString(l.statusColor(l.status))
if l.details != "" {
sb.WriteString(" ")
sb.WriteString(l.details)
}
sb.WriteString(strings.Repeat(" ", l.timerPad))
sb.WriteString(TimerColor(l.timer))
sb.WriteString("\n")
return sb.String()
}
var (
spinnerDone = "✔"
spinnerWarning = "!"
spinnerError = "✘"
)
func spinner(t *task) string {
switch t.status {
case api.Done:
return SuccessColor(spinnerDone)
case api.Warning:
return WarningColor(spinnerWarning)
case api.Error:
return ErrorColor(spinnerError)
default:
return CountColor(t.spinner.String())
}
}
func colorFn(s api.EventStatus) colorFunc {
switch s {
case api.Done:
return SuccessColor
case api.Warning:
return WarningColor
case api.Error:
return ErrorColor
default:
return nocolor
}
}
func numDone(tasks map[string]*task) int {
i := 0
for _, t := range tasks {
if t.status != api.Working {
i++
}
}
return i
}
// lenAnsi count of user-perceived characters in ANSI string.
func lenAnsi(s string) int {
length := 0
ansiCode := false
for _, r := range s {
if r == '\x1b' {
ansiCode = true
continue
}
if ansiCode && r == 'm' {
ansiCode = false
continue
}
if !ansiCode {
length++
}
}
return length
}
var percentChars = strings.Split("⠀⡀⣀⣄⣤⣦⣶⣷⣿", "")
================================================
FILE: cmd/display/tty_test.go
================================================
/*
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 display
import (
"bytes"
"context"
"strings"
"sync"
"testing"
"time"
"unicode/utf8"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func newTestWriter() (*ttyWriter, *bytes.Buffer) {
var buf bytes.Buffer
w := &ttyWriter{
out: &buf,
info: &buf,
tasks: map[string]*task{},
done: make(chan bool),
mtx: &sync.Mutex{},
operation: "pull",
}
return w, &buf
}
func addTask(w *ttyWriter, id, text, details string, status api.EventStatus) {
t := &task{
ID: id,
parents: make(map[string]struct{}),
startTime: time.Now(),
text: text,
details: details,
status: status,
spinner: NewSpinner(),
}
w.tasks[id] = t
w.ids = append(w.ids, id)
}
// extractLines parses the output buffer and returns lines without ANSI control sequences
func extractLines(buf *bytes.Buffer) []string {
content := buf.String()
// Split by newline
rawLines := strings.Split(content, "\n")
var lines []string
for _, line := range rawLines {
// Skip empty lines and lines that are just ANSI codes
if lenAnsi(line) > 0 {
lines = append(lines, line)
}
}
return lines
}
func TestPrintWithDimensions_LinesFitTerminalWidth(t *testing.T) {
testCases := []struct {
name string
taskID string
status string
details string
terminalWidth int
}{
{
name: "short task fits wide terminal",
taskID: "Image foo",
status: "Pulling",
details: "layer abc123",
terminalWidth: 100,
},
{
name: "long details truncated to fit",
taskID: "Image foo",
status: "Pulling",
details: "downloading layer sha256:abc123def456789xyz0123456789abcdef",
terminalWidth: 50,
},
{
name: "long taskID truncated to fit",
taskID: "very-long-image-name-that-exceeds-terminal-width",
status: "Pulling",
details: "",
terminalWidth: 40,
},
{
name: "both long taskID and details",
taskID: "my-very-long-service-name-here",
status: "Downloading",
details: "layer sha256:abc123def456789xyz0123456789",
terminalWidth: 50,
},
{
name: "narrow terminal",
taskID: "service-name",
status: "Pulling",
details: "some details",
terminalWidth: 35,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w, buf := newTestWriter()
addTask(w, tc.taskID, tc.status, tc.details, api.Working)
w.printWithDimensions(tc.terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= tc.terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, tc.terminalWidth, line)
}
})
}
}
func TestPrintWithDimensions_MultipleTasksFitTerminalWidth(t *testing.T) {
w, buf := newTestWriter()
// Add multiple tasks with varying lengths
addTask(w, "Image nginx", "Pulling", "layer sha256:abc123", api.Working)
addTask(w, "Image postgres-database", "Pulling", "downloading", api.Working)
addTask(w, "Image redis", "Pulled", "", api.Done)
terminalWidth := 60
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestPrintWithDimensions_VeryNarrowTerminal(t *testing.T) {
w, buf := newTestWriter()
addTask(w, "Image nginx", "Pulling", "details", api.Working)
terminalWidth := 30
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestPrintWithDimensions_TaskWithProgress(t *testing.T) {
w, buf := newTestWriter()
// Create parent task
parent := &task{
ID: "Image nginx",
parents: make(map[string]struct{}),
startTime: time.Now(),
text: "Pulling",
status: api.Working,
spinner: NewSpinner(),
}
w.tasks["Image nginx"] = parent
w.ids = append(w.ids, "Image nginx")
// Create child tasks to trigger progress display
for i := range 3 {
child := &task{
ID: "layer" + string(rune('a'+i)),
parents: map[string]struct{}{"Image nginx": {}},
startTime: time.Now(),
text: "Downloading",
status: api.Working,
total: 1000,
current: 500,
percent: 50,
spinner: NewSpinner(),
}
w.tasks[child.ID] = child
w.ids = append(w.ids, child.ID)
}
terminalWidth := 80
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestAdjustLineWidth_DetailsCorrectlyTruncated(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "Image foo",
status: "Pulling",
details: "downloading layer sha256:abc123def456789xyz",
},
}
terminalWidth := 50
timerLen := 5
w.adjustLineWidth(lines, timerLen, terminalWidth)
// Verify the line fits
detailsLen := len(lines[0].details)
if detailsLen > 0 {
detailsLen++ // space before details
}
// widthWithoutDetails = 5 + prefix(0) + taskID(9) + progress(0) + status(7) + timer(5) = 26
lineWidth := 5 + len(lines[0].taskID) + len(lines[0].status) + detailsLen + timerLen
assert.Assert(t, lineWidth <= terminalWidth,
"line width %d should not exceed terminal width %d (taskID=%q, details=%q)",
lineWidth, terminalWidth, lines[0].taskID, lines[0].details)
// Verify details were truncated (not removed entirely)
assert.Assert(t, lines[0].details != "", "details should be truncated, not removed")
assert.Assert(t, strings.HasSuffix(lines[0].details, "..."), "truncated details should end with ...")
}
func TestAdjustLineWidth_TaskIDCorrectlyTruncated(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "very-long-image-name-that-exceeds-minimum-length",
status: "Pulling",
details: "",
},
}
terminalWidth := 40
timerLen := 5
w.adjustLineWidth(lines, timerLen, terminalWidth)
lineWidth := 5 + len(lines[0].taskID) + 7 + timerLen
assert.Assert(t, lineWidth <= terminalWidth,
"line width %d should not exceed terminal width %d (taskID=%q)",
lineWidth, terminalWidth, lines[0].taskID)
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."), "truncated taskID should end with ...")
}
func TestAdjustLineWidth_NoTruncationNeeded(t *testing.T) {
w := &ttyWriter{}
originalDetails := "short"
originalTaskID := "Image foo"
lines := []lineData{
{
taskID: originalTaskID,
status: "Pulling",
details: originalDetails,
},
}
// Wide terminal, nothing should be truncated
w.adjustLineWidth(lines, 5, 100)
assert.Equal(t, originalTaskID, lines[0].taskID, "taskID should not be modified")
assert.Equal(t, originalDetails, lines[0].details, "details should not be modified")
}
func TestAdjustLineWidth_DetailsRemovedWhenTooShort(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "Image foo",
status: "Pulling",
details: "abc", // Very short, can't be meaningfully truncated
},
}
// Terminal so narrow that even minimal details + "..." wouldn't help
w.adjustLineWidth(lines, 5, 28)
assert.Equal(t, "", lines[0].details, "details should be removed entirely when too short to truncate")
}
// stripAnsi removes ANSI escape codes from a string
func stripAnsi(s string) string {
var result strings.Builder
inAnsi := false
for _, r := range s {
if r == '\x1b' {
inAnsi = true
continue
}
if inAnsi {
// ANSI sequences end with a letter (m, h, l, G, etc.)
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
inAnsi = false
}
continue
}
result.WriteRune(r)
}
return result.String()
}
func TestPrintWithDimensions_PulledAndPullingWithLongIDs(t *testing.T) {
w, buf := newTestWriter()
// Add a completed task with long ID
completedTask := &task{
ID: "Image docker.io/library/nginx-long-name",
parents: make(map[string]struct{}),
startTime: time.Now().Add(-2 * time.Second),
endTime: time.Now(),
text: "Pulled",
status: api.Done,
spinner: NewSpinner(),
}
completedTask.spinner.Stop()
w.tasks[completedTask.ID] = completedTask
w.ids = append(w.ids, completedTask.ID)
// Add a pending task with long ID
pendingTask := &task{
ID: "Image docker.io/library/postgres-database",
parents: make(map[string]struct{}),
startTime: time.Now(),
text: "Pulling",
status: api.Working,
spinner: NewSpinner(),
}
w.tasks[pendingTask.ID] = pendingTask
w.ids = append(w.ids, pendingTask.ID)
terminalWidth := 50
w.printWithDimensions(terminalWidth, 24)
// Strip all ANSI codes from output and split by newline
stripped := stripAnsi(buf.String())
lines := strings.Split(stripped, "\n")
// Filter non-empty lines
var nonEmptyLines []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
nonEmptyLines = append(nonEmptyLines, line)
}
}
// Expected output format (50 runes per task line)
expected := `[+] pull 1/2
✔ Image docker.io/library/nginx-l... Pulled 2.0s
⠋ Image docker.io/library/postgre... Pulling 0.0s`
expectedLines := strings.Split(expected, "\n")
// Debug output
t.Logf("Actual output:\n")
for i, line := range nonEmptyLines {
t.Logf(" line %d (%2d runes): %q", i, utf8.RuneCountInString(line), line)
}
// Verify number of lines
assert.Equal(t, len(expectedLines), len(nonEmptyLines), "number of lines should match")
// Verify each line matches expected
for i, line := range nonEmptyLines {
if i < len(expectedLines) {
assert.Equal(t, expectedLines[i], line,
"line %d should match expected", i)
}
}
// Verify task lines fit within terminal width (strict - no tolerance)
for i, line := range nonEmptyLines {
if i > 0 { // Skip header line
runeCount := utf8.RuneCountInString(line)
assert.Assert(t, runeCount <= terminalWidth,
"line %d has %d runes which exceeds terminal width %d: %q",
i, runeCount, terminalWidth, line)
}
}
}
func TestLenAnsi(t *testing.T) {
testCases := []struct {
input string
expected int
}{
{"hello", 5},
{"\x1b[32mhello\x1b[0m", 5},
{"\x1b[1;32mgreen\x1b[0m text", 10},
{"", 0},
{"\x1b[0m", 0},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := lenAnsi(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestDoneDeadlockFix(t *testing.T) {
w, _ := newTestWriter()
addTask(w, "test-task", "Working", "details", api.Working)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
w.Start(ctx, "test")
done := make(chan bool)
go func() {
w.Done("test", true)
done <- true
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Deadlock detected: Done() did not complete within 5 seconds")
}
}
================================================
FILE: cmd/formatter/ansi.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"github.com/acarl005/stripansi"
"github.com/morikuni/aec"
)
var disableAnsi bool
func saveCursor() {
if disableAnsi {
return
}
// see https://github.com/morikuni/aec/pull/5
fmt.Print(aec.Save)
}
func restoreCursor() {
if disableAnsi {
return
}
// see https://github.com/morikuni/aec/pull/5
fmt.Print(aec.Restore)
}
func showCursor() {
if disableAnsi {
return
}
fmt.Print(aec.Show)
}
func moveCursor(y, x int) {
if disableAnsi {
return
}
fmt.Print(aec.Position(uint(y), uint(x)))
}
func carriageReturn() {
if disableAnsi {
return
}
fmt.Print(aec.Column(0))
}
func clearLine() {
if disableAnsi {
return
}
// Does not move cursor from its current position
fmt.Print(aec.EraseLine(aec.EraseModes.Tail))
}
func moveCursorUp(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(aec.Up(uint(lines)))
}
func moveCursorDown(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(aec.Down(uint(lines)))
}
func newLine() {
// Like \n
fmt.Print("\012")
}
func lenAnsi(s string) int {
// len has into consideration ansi codes, if we want
// the len of the actual len(string) we need to strip
// all ansi codes
return len(stripansi.Strip(s))
}
================================================
FILE: cmd/formatter/colors.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/docker/cli/cli/command"
)
var names = []string{
"grey",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
}
const (
BOLD = "1"
FAINT = "2"
ITALIC = "3"
UNDERLINE = "4"
)
const (
RESET = "0"
CYAN = "36"
)
const (
// Never use ANSI codes
Never = "never"
// Always use ANSI codes
Always = "always"
// Auto detect terminal is a tty and can use ANSI codes
Auto = "auto"
)
// ansiColorOffset is the offset for basic foreground colors in ANSI escape codes.
const ansiColorOffset = 30
// SetANSIMode configure formatter for colored output on ANSI-compliant console
func SetANSIMode(streams command.Streams, ansi string) {
if !useAnsi(streams, ansi) {
nextColor = func() colorFunc {
return monochrome
}
disableAnsi = true
}
}
func useAnsi(streams command.Streams, ansi string) bool {
switch ansi {
case Always:
return true
case Auto:
return streams.Out().IsTerminal()
}
return false
}
// colorFunc use ANSI codes to render colored text on console
type colorFunc func(s string) string
var monochrome = func(s string) string {
return s
}
func ansiColor(code, s string, formatOpts ...string) string {
return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0"))
}
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
func ansiColorCode(code string, formatOpts ...string) string {
var sb strings.Builder
sb.WriteString("\033[")
for _, c := range formatOpts {
sb.WriteString(c)
sb.WriteString(";")
}
sb.WriteString(code)
sb.WriteString("m")
return sb.String()
}
func makeColorFunc(code string) colorFunc {
return func(s string) string {
return ansiColor(code, s)
}
}
var (
nextColor = rainbowColor
rainbow []colorFunc
currentIndex = 0
mutex sync.Mutex
)
func rainbowColor() colorFunc {
mutex.Lock()
defer mutex.Unlock()
result := rainbow[currentIndex]
currentIndex = (currentIndex + 1) % len(rainbow)
return result
}
func init() {
colors := map[string]colorFunc{}
for i, name := range names {
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
}
rainbow = []colorFunc{
colors["cyan"],
colors["yellow"],
colors["green"],
colors["magenta"],
colors["blue"],
colors["intense_cyan"],
colors["intense_yellow"],
colors["intense_green"],
colors["intense_magenta"],
colors["intense_blue"],
}
}
================================================
FILE: cmd/formatter/consts.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
const (
// JSON Print in JSON format
JSON = "json"
// TemplateLegacyJSON the legacy json formatting value using go template
TemplateLegacyJSON = "{{json.}}"
// PRETTY is the constant for default formats on list commands
//
// Deprecated: use TABLE
PRETTY = "pretty"
// TABLE Print output in table format with column headers (default)
TABLE = "table"
)
================================================
FILE: cmd/formatter/container.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"net/netip"
"strconv"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client/pkg/stringid"
"github.com/docker/compose/v5/pkg/api"
)
const (
defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
nameHeader = "NAME"
projectHeader = "PROJECT"
serviceHeader = "SERVICE"
commandHeader = "COMMAND"
runningForHeader = "CREATED"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
)
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
switch source {
case formatter.TableFormatKey, "": // table formatting is the default if none is set.
if quiet {
return formatter.DefaultQuietFormat
}
format := defaultContainerTableFormat
if size {
format += `\t{{.Size}}`
}
return formatter.Format(format)
case formatter.RawFormatKey:
if quiet {
return `container_id: {{.ID}}`
}
format := `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
state: {{- pad .State 1 0}}
status: {{- pad .Status 1 0}}
names: {{.Names}}
labels: {{- pad .Labels 1 0}}
ports: {{- pad .Ports 1 0}}
`
if size {
format += `size: {{.Size}}\n`
}
return formatter.Format(format)
default: // custom format
if quiet {
return formatter.DefaultQuietFormat
}
return formatter.Format(source)
}
}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, container := range containers {
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(NewContainerContext(), render)
}
// ContainerContext is a struct used for rendering a list of containers in a Go template.
type ContainerContext struct {
formatter.HeaderContext
trunc bool
c api.ContainerSummary
// FieldsUsed is used in the pre-processing step to detect which fields are
// used in the template. It's currently only used to detect use of the .Size
// field which (if used) automatically sets the '--size' option when making
// the API call.
FieldsUsed map[string]any
}
// NewContainerContext creates a new context for rendering containers
func NewContainerContext() *ContainerContext {
containerCtx := ContainerContext{}
containerCtx.Header = formatter.SubHeaderContext{
"ID": formatter.ContainerIDHeader,
"Name": nameHeader,
"Project": projectHeader,
"Service": serviceHeader,
"Image": formatter.ImageHeader,
"Command": commandHeader,
"CreatedAt": formatter.CreatedAtHeader,
"RunningFor": runningForHeader,
"Ports": formatter.PortsHeader,
"State": formatter.StateHeader,
"Status": formatter.StatusHeader,
"Size": formatter.SizeHeader,
"Labels": formatter.LabelsHeader,
}
return &containerCtx
}
// MarshalJSON makes ContainerContext implement json.Marshaler
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c)
}
// ID returns the container's ID as a string. Depending on the `--no-trunc`
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
return c.c.ID
}
func (c *ContainerContext) Name() string {
return c.c.Name
}
// Names returns a comma-separated string of the container's names, with their
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted.
func (c *ContainerContext) Names() string {
names := formatter.StripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
return strings.Join(names, ",")
}
func (c *ContainerContext) Service() string {
return c.c.Service
}
func (c *ContainerContext) Project() string {
return c.c.Project
}
func (c *ContainerContext) Image() string {
return c.c.Image
}
func (c *ContainerContext) Command() string {
command := c.c.Command
if c.trunc {
command = formatter.Ellipsis(command, 20)
}
return strconv.Quote(command)
}
func (c *ContainerContext) CreatedAt() string {
return time.Unix(c.c.Created, 0).String()
}
func (c *ContainerContext) RunningFor() string {
createdAt := time.Unix(c.c.Created, 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *ContainerContext) ExitCode() int {
return c.c.ExitCode
}
func (c *ContainerContext) State() string {
return string(c.c.State)
}
func (c *ContainerContext) Status() string {
return c.c.Status
}
func (c *ContainerContext) Health() string {
return string(c.c.Health)
}
func (c *ContainerContext) Publishers() api.PortPublishers {
return c.c.Publishers
}
func (c *ContainerContext) Ports() string {
var ports []container.PortSummary
for _, publisher := range c.c.Publishers {
var pIP netip.Addr
if publisher.URL != "" {
if p, err := netip.ParseAddr(publisher.URL); err == nil {
pIP = p
}
}
ports = append(ports, container.PortSummary{
IP: pIP,
PrivatePort: uint16(publisher.TargetPort),
PublicPort: uint16(publisher.PublishedPort),
Type: publisher.Protocol,
})
}
return formatter.DisplayablePorts(ports)
}
// Labels returns a comma-separated string of labels present on the container.
func (c *ContainerContext) Labels() string {
if c.c.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
// Label returns the value of the label with the given name or an empty string
// if the given label does not exist.
func (c *ContainerContext) Label(name string) string {
if c.c.Labels == nil {
return ""
}
return c.c.Labels[name]
}
// Mounts returns a comma-separated string of mount names present on the container.
// If the trunc option is set, names can be truncated (ellipsized).
func (c *ContainerContext) Mounts() string {
var mounts []string
for _, name := range c.c.Mounts {
if c.trunc {
name = formatter.Ellipsis(name, 15)
}
mounts = append(mounts, name)
}
return strings.Join(mounts, ",")
}
// LocalVolumes returns the number of volumes using the "local" volume driver.
func (c *ContainerContext) LocalVolumes() string {
return fmt.Sprintf("%d", c.c.LocalVolumes)
}
// Networks returns a comma-separated string of networks that the container is
// attached to.
func (c *ContainerContext) Networks() string {
return strings.Join(c.c.Networks, ",")
}
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
func (c *ContainerContext) Size() string {
if c.FieldsUsed == nil {
c.FieldsUsed = map[string]any{}
}
c.FieldsUsed["Size"] = struct{}{}
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}
================================================
FILE: cmd/formatter/formatter.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"io"
"reflect"
"strings"
"github.com/docker/compose/v5/pkg/api"
)
// Print prints formatted lists in different formats
func Print(toJSON any, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
switch strings.ToLower(format) {
case TABLE, PRETTY, "":
return PrintPrettySection(outWriter, writerFn, headers...)
case TemplateLegacyJSON:
switch reflect.TypeOf(toJSON).Kind() {
case reflect.Slice:
s := reflect.ValueOf(toJSON)
for i := 0; i < s.Len(); i++ {
obj := s.Index(i).Interface()
outJSON, err := ToJSON(obj, "", "")
if err != nil {
return err
}
_, _ = fmt.Fprint(outWriter, outJSON)
}
default:
outJSON, err := ToStandardJSON(toJSON)
if err != nil {
return err
}
_, _ = fmt.Fprintln(outWriter, outJSON)
}
case JSON:
switch reflect.TypeOf(toJSON).Kind() {
case reflect.Slice:
outJSON, err := ToJSON(toJSON, "", "")
if err != nil {
return err
}
_, _ = fmt.Fprint(outWriter, outJSON)
default:
outJSON, err := ToStandardJSON(toJSON)
if err != nil {
return err
}
_, _ = fmt.Fprintln(outWriter, outJSON)
}
default:
return fmt.Errorf("format value %q could not be parsed: %w", format, api.ErrParsingFailed)
}
return nil
}
================================================
FILE: cmd/formatter/formatter_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"bytes"
"fmt"
"io"
"testing"
"go.uber.org/goleak"
"gotest.tools/v3/assert"
)
type testStruct struct {
Name string
Status string
}
// Print prints formatted lists in different formats
func TestPrint(t *testing.T) {
testList := []testStruct{
{
Name: "myName1",
Status: "myStatus1",
},
{
Name: "myName2",
Status: "myStatus2",
},
}
b := &bytes.Buffer{}
assert.NilError(t, Print(testList, TABLE, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
assert.Equal(t, b.String(), "NAME STATUS\nmyName1 myStatus1\nmyName2 myStatus2\n")
b.Reset()
assert.NilError(t, Print(testList, JSON, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
assert.Equal(t, b.String(), `[{"Name":"myName1","Status":"myStatus1"},{"Name":"myName2","Status":"myStatus2"}]
`)
b.Reset()
assert.NilError(t, Print(testList, TemplateLegacyJSON, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
json := b.String()
assert.Equal(t, json, `{"Name":"myName1","Status":"myStatus1"}
{"Name":"myName2","Status":"myStatus2"}
`)
}
func TestColorsGoroutinesLeak(t *testing.T) {
goleak.VerifyNone(t)
}
================================================
FILE: cmd/formatter/json.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"bytes"
"encoding/json"
)
const standardIndentation = " "
// ToStandardJSON return a string with the JSON representation of the interface{}
func ToStandardJSON(i any) (string, error) {
return ToJSON(i, "", standardIndentation)
}
// ToJSON return a string with the JSON representation of the interface{}
func ToJSON(i any, prefix string, indentation string) (string, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
encoder.SetIndent(prefix, indentation)
err := encoder.Encode(i)
return buffer.String(), err
}
================================================
FILE: cmd/formatter/logs.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
"github.com/buger/goterm"
"github.com/moby/moby/client/pkg/jsonmessage"
"github.com/docker/compose/v5/pkg/api"
)
// LogConsumer consume logs from services and format them
type logConsumer struct {
ctx context.Context
presenters sync.Map // map[string]*presenter
width int
stdout io.Writer
stderr io.Writer
color bool
prefix bool
timestamp bool
}
// NewLogConsumer creates a new LogConsumer
func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix, timestamp bool) api.LogConsumer {
return &logConsumer{
ctx: ctx,
presenters: sync.Map{},
width: 0,
stdout: stdout,
stderr: stderr,
color: color,
prefix: prefix,
timestamp: timestamp,
}
}
func (l *logConsumer) register(name string) *presenter {
var p *presenter
root, _, found := strings.Cut(name, " ")
if found {
parent := l.getPresenter(root)
p = &presenter{
colors: parent.colors,
name: name,
prefix: parent.prefix,
}
} else {
cf := monochrome
if l.color {
switch name {
case "":
cf = monochrome
case api.WatchLogger:
cf = makeColorFunc("92")
default:
cf = nextColor()
}
}
p = &presenter{
colors: cf,
name: name,
}
}
l.presenters.Store(name, p)
l.computeWidth()
if l.prefix {
l.presenters.Range(func(key, value any) bool {
p := value.(*presenter)
p.setPrefix(l.width)
return true
})
}
return p
}
func (l *logConsumer) getPresenter(container string) *presenter {
p, ok := l.presenters.Load(container)
if !ok { // should have been registered, but ¯\_(ツ)_/¯
return l.register(container)
}
return p.(*presenter)
}
// Log formats a log message as received from name/container
func (l *logConsumer) Log(container, message string) {
l.write(l.stdout, container, message)
}
// Err formats a log message as received from name/container
func (l *logConsumer) Err(container, message string) {
l.write(l.stderr, container, message)
}
func (l *logConsumer) write(w io.Writer, container, message string) {
if l.ctx.Err() != nil {
return
}
p := l.getPresenter(container)
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
for line := range strings.SplitSeq(message, "\n") {
if l.timestamp {
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
} else {
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
}
}
}
func (l *logConsumer) Status(container, msg string) {
p := l.getPresenter(container)
s := p.colors(fmt.Sprintf("%s%s %s\n", goterm.RESET_LINE, container, msg))
l.stdout.Write([]byte(s)) //nolint:errcheck
}
func (l *logConsumer) computeWidth() {
width := 0
l.presenters.Range(func(key, value any) bool {
p := value.(*presenter)
if len(p.name) > width {
width = len(p.name)
}
return true
})
l.width = width + 1
}
type presenter struct {
colors colorFunc
name string
prefix string
}
func (p *presenter) setPrefix(width int) {
if p.name == api.WatchLogger {
p.prefix = p.colors(strings.Repeat(" ", width) + " ⦿ ")
return
}
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
}
type logDecorator struct {
decorated api.LogConsumer
Before func()
After func()
}
func (l logDecorator) Log(containerName, message string) {
l.Before()
l.decorated.Log(containerName, message)
l.After()
}
func (l logDecorator) Err(containerName, message string) {
l.Before()
l.decorated.Err(containerName, message)
l.After()
}
func (l logDecorator) Status(container, msg string) {
l.Before()
l.decorated.Status(container, msg)
l.After()
}
================================================
FILE: cmd/formatter/pretty.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"io"
"strings"
"text/tabwriter"
)
// PrintPrettySection prints a tabbed section on the writer parameter
func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error {
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
printer(w)
return w.Flush()
}
================================================
FILE: cmd/formatter/shortcut.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"context"
"errors"
"fmt"
"math"
"os"
"strings"
"syscall"
"time"
"github.com/buger/goterm"
"github.com/compose-spec/compose-go/v2/types"
"github.com/eiannone/keyboard"
"github.com/skratchdot/open-golang/open"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
)
const DISPLAY_ERROR_TIME = 10
type KeyboardError struct {
err error
timeStart time.Time
}
func (ke *KeyboardError) shouldDisplay() bool {
return ke.err != nil && int(time.Since(ke.timeStart).Seconds()) < DISPLAY_ERROR_TIME
}
func (ke *KeyboardError) printError(height int, info string) {
if ke.shouldDisplay() {
errMessage := ke.err.Error()
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
clearLine()
fmt.Print(errMessage)
}
}
func (ke *KeyboardError) addError(prefix string, err error) {
ke.timeStart = time.Now()
prefix = ansiColor(CYAN, fmt.Sprintf("%s →", prefix), BOLD)
errorString := fmt.Sprintf("%s %s", prefix, err.Error())
ke.err = errors.New(errorString)
}
func (ke *KeyboardError) error() string {
return ke.err.Error()
}
type KeyboardWatch struct {
Watching bool
Watcher Feature
}
// Feature is an compose feature that can be started/stopped by a menu command
type Feature interface {
Start(context.Context) error
Stop() error
}
type KEYBOARD_LOG_LEVEL int
const (
NONE KEYBOARD_LOG_LEVEL = 0
INFO KEYBOARD_LOG_LEVEL = 1
DEBUG KEYBOARD_LOG_LEVEL = 2
)
type LogKeyboard struct {
kError KeyboardError
Watch *KeyboardWatch
Detach func()
IsDockerDesktopActive bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
}
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
return &LogKeyboard{
IsDockerDesktopActive: isDockerDesktopActive,
logLevel: INFO,
signalChannel: sc,
}
}
func (lk *LogKeyboard) Decorate(l api.LogConsumer) api.LogConsumer {
return logDecorator{
decorated: l,
Before: lk.clearNavigationMenu,
After: lk.PrintKeyboardInfo,
}
}
func (lk *LogKeyboard) PrintKeyboardInfo() {
if lk.logLevel == INFO {
lk.printNavigationMenu()
}
}
// Creates space to print error and menu string
func (lk *LogKeyboard) createBuffer(lines int) {
if lk.kError.shouldDisplay() {
extraLines := extraLines(lk.kError.error()) + 1
lines += extraLines
}
// get the string
infoMessage := lk.navigationMenu()
// calculate how many lines we need to display the menu info
// might be needed a line break
extraLines := extraLines(infoMessage) + 1
lines += extraLines
if lines > 0 {
allocateSpace(lines)
moveCursorUp(lines)
}
}
func (lk *LogKeyboard) printNavigationMenu() {
offset := 1
lk.clearNavigationMenu()
lk.createBuffer(offset)
if lk.logLevel == INFO {
height := goterm.Height()
menu := lk.navigationMenu()
carriageReturn()
saveCursor()
lk.kError.printError(height, menu)
moveCursor(height-extraLines(menu), 0)
clearLine()
fmt.Print(menu)
carriageReturn()
restoreCursor()
}
}
func (lk *LogKeyboard) navigationMenu() string {
var items []string
if lk.IsDockerDesktopActive {
items = append(items, shortcutKeyColor("v")+navColor(" View in Docker Desktop"))
}
if lk.IsDockerDesktopActive {
items = append(items, shortcutKeyColor("o")+navColor(" View Config"))
}
isEnabled := " Enable"
if lk.Watch != nil && lk.Watch.Watching {
isEnabled = " Disable"
}
items = append(items, shortcutKeyColor("w")+navColor(isEnabled+" Watch"))
items = append(items, shortcutKeyColor("d")+navColor(" Detach"))
return strings.Join(items, " ")
}
func (lk *LogKeyboard) clearNavigationMenu() {
height := goterm.Height()
carriageReturn()
saveCursor()
// clearLine()
for range height {
moveCursorDown(1)
clearLine()
}
restoreCursor()
}
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
if !lk.IsDockerDesktopActive {
return
}
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop")
lk.keyboardError("View", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
if !lk.IsDockerDesktopActive {
return
}
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("View Config", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("Watch Docs", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
lk.kError.addError(prefix, err)
lk.printNavigationMenu()
timer1 := time.NewTimer((DISPLAY_ERROR_TIME + 1) * time.Second)
go func() {
<-timer1.C
lk.printNavigationMenu()
}()
}
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
if lk.Watch == nil {
return
}
if lk.Watch.Watching {
err := lk.Watch.Watcher.Stop()
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = false
}
} else {
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := lk.Watch.Watcher.Start(ctx)
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = true
}
return err
})()
}()
}
}
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
switch kRune := event.Rune; kRune {
case 'd':
lk.clearNavigationMenu()
lk.Detach()
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
if lk.Watch == nil {
// we try to open watch docs if DD is installed
if lk.IsDockerDesktopActive {
lk.openDDWatchDocs(ctx, project)
}
// either way we mark menu/watch as an error
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
})()
}()
}
lk.ToggleWatch(ctx, options)
case 'o':
lk.openDDComposeUI(ctx, project)
}
switch key := event.Key; key {
case keyboard.KeyCtrlC:
_ = keyboard.Close()
lk.clearNavigationMenu()
showCursor()
lk.logLevel = NONE
// will notify main thread to kill and will handle gracefully
lk.signalChannel <- syscall.SIGINT
case keyboard.KeyCtrlZ:
handleCtrlZ()
case keyboard.KeyEnter:
newLine()
lk.printNavigationMenu()
}
}
func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
lk.Watch = &KeyboardWatch{
Watching: enabled,
Watcher: watcher,
}
}
func (lk *LogKeyboard) EnableDetach(detach func()) {
lk.Detach = detach
}
func allocateSpace(lines int) {
for range lines {
clearLine()
newLine()
carriageReturn()
}
}
func extraLines(s string) int {
return int(math.Floor(float64(lenAnsi(s)) / float64(goterm.Width())))
}
func shortcutKeyColor(key string) string {
foreground := "38;2"
black := "0;0;0"
background := "48;2"
white := "255;255;255"
return ansiColor(foreground+";"+black+";"+background+";"+white, key, BOLD)
}
func navColor(key string) string {
return ansiColor(FAINT, key)
}
================================================
FILE: cmd/formatter/shortcut_unix.go
================================================
//go:build !windows
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import "syscall"
func handleCtrlZ() {
_ = syscall.Kill(0, syscall.SIGSTOP)
}
================================================
FILE: cmd/formatter/shortcut_windows.go
================================================
//go:build windows
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
// handleCtrlZ is a no-op on Windows as SIGSTOP is not supported
func handleCtrlZ() {
// Windows doesn't support SIGSTOP/SIGCONT signals
// Ctrl+Z behavior is handled differently by the Windows terminal
}
================================================
FILE: cmd/main.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/cmdtrace"
"github.com/docker/compose/v5/cmd/compatibility"
commands "github.com/docker/compose/v5/cmd/compose"
"github.com/docker/compose/v5/cmd/prompt"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/pkg/compose"
)
func pluginMain() {
plugin.Run(
func(cli command.Cli) *cobra.Command {
backendOptions := &commands.BackendOptions{
Options: []compose.Option{
compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
},
}
cmd := commands.RootCommand(cli, backendOptions)
originalPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the cli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if err := cmdtrace.Setup(cmd, cli, os.Args[1:]); err != nil {
logrus.Debugf("failed to enable tracing: %v", err)
}
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
}
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{
StatusCode: 1,
Status: err.Error(),
}
})
return cmd
},
metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,
},
command.WithUserAgent("compose/"+internal.Version),
)
}
func main() {
if plugin.RunningStandalone() {
os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
}
pluginMain()
}
================================================
FILE: cmd/prompt/prompt.go
================================================
/*
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 prompt
import (
"fmt"
"io"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v5/pkg/utils"
)
//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v5/pkg/prompt" -package=prompt . UI
// UI - prompt user input
type UI interface {
Confirm(message string, defaultValue bool) (bool, error)
}
func NewPrompt(stdin *streams.In, stdout *streams.Out) UI {
if stdin.IsTerminal() {
return User{stdin: streamsFileReader{stdin}, stdout: streamsFileWriter{stdout}}
}
return Pipe{stdin: stdin, stdout: stdout}
}
// User - in a terminal
type User struct {
stdout streamsFileWriter
stdin streamsFileReader
}
// adapt streams.Out to terminal.FileWriter
type streamsFileWriter struct {
stream *streams.Out
}
func (s streamsFileWriter) Write(p []byte) (n int, err error) {
return s.stream.Write(p)
}
func (s streamsFileWriter) Fd() uintptr {
return s.stream.FD()
}
// adapt streams.In to terminal.FileReader
type streamsFileReader struct {
stream *streams.In
}
func (s streamsFileReader) Read(p []byte) (n int, err error) {
return s.stream.Read(p)
}
func (s streamsFileReader) Fd() uintptr {
return s.stream.FD()
}
// Confirm asks for yes or no input
func (u User) Confirm(message string, defaultValue bool) (bool, error) {
qs := &survey.Confirm{
Message: message,
Default: defaultValue,
}
var b bool
err := survey.AskOne(qs, &b, func(options *survey.AskOptions) error {
options.Stdio.In = u.stdin
options.Stdio.Out = u.stdout
return nil
})
return b, err
}
// Pipe - aggregates prompt methods
type Pipe struct {
stdout io.Writer
stdin io.Reader
}
// Confirm asks for yes or no input
func (u Pipe) Confirm(message string, defaultValue bool) (bool, error) {
_, _ = fmt.Fprint(u.stdout, message)
var answer string
_, _ = fmt.Fscanln(u.stdin, &answer)
return utils.StringToBool(answer), nil
}
================================================
FILE: cmd/prompt/prompt_mock.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/docker/compose-cli/pkg/prompt (interfaces: UI)
// Package prompt is a generated GoMock package.
package prompt
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockUI is a mock of UI interface
type MockUI struct {
ctrl *gomock.Controller
recorder *MockUIMockRecorder
}
// MockUIMockRecorder is the mock recorder for MockUI
type MockUIMockRecorder struct {
mock *MockUI
}
// NewMockUI creates a new mock instance
func NewMockUI(ctrl *gomock.Controller) *MockUI {
mock := &MockUI{ctrl: ctrl}
mock.recorder = &MockUIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUI) EXPECT() *MockUIMockRecorder {
return m.recorder
}
// Confirm mocks base method
func (m *MockUI) Confirm(arg0 string, arg1 bool) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Confirm", arg0, arg1)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Confirm indicates an expected call of Confirm
func (mr *MockUIMockRecorder) Confirm(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Confirm", reflect.TypeOf((*MockUI)(nil).Confirm), arg0, arg1)
}
// Input mocks base method
func (m *MockUI) Input(arg0, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Input", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Input indicates an expected call of Input
func (mr *MockUIMockRecorder) Input(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Input", reflect.TypeOf((*MockUI)(nil).Input), arg0, arg1)
}
// Password mocks base method
func (m *MockUI) Password(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Password", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Password indicates an expected call of Password
func (mr *MockUIMockRecorder) Password(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Password", reflect.TypeOf((*MockUI)(nil).Password), arg0)
}
// Select mocks base method
func (m *MockUI) Select(arg0 string, arg1 []string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", arg0, arg1)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Select indicates an expected call of Select
func (mr *MockUIMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockUI)(nil).Select), arg0, arg1)
}
================================================
FILE: codecov.yml
================================================
coverage:
status:
project:
default:
informational: true
target: auto
threshold: 2%
patch:
default:
informational: true
comment:
require_changes: true
ignore:
- "packaging"
- "docs"
- "bin"
- "e2e"
- "pkg/e2e"
- "**/*_test.go"
================================================
FILE: docker-bake.hcl
================================================
// Copyright 2022 Docker Compose CLI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
variable "GO_VERSION" {
# default ARG value set in Dockerfile
default = null
}
variable "BUILD_TAGS" {
default = "e2e"
}
variable "DOCS_FORMATS" {
default = "md,yaml"
}
# Defines the output folder to override the default behavior.
# See Makefile for details, this is generally only useful for
# the packaging scripts and care should be taken to not break
# them.
variable "DESTDIR" {
default = ""
}
function "outdir" {
params = [defaultdir]
result = DESTDIR != "" ? DESTDIR : "${defaultdir}"
}
# Special target: https://github.com/docker/metadata-action#bake-definition
target "meta-helper" {}
target "_common" {
args = {
GO_VERSION = GO_VERSION
BUILD_TAGS = BUILD_TAGS
BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1
}
}
group "default" {
targets = ["binary"]
}
group "validate" {
targets = ["lint", "vendor-validate", "license-validate"]
}
target "lint" {
inherits = ["_common"]
target = "lint"
output = ["type=cacheonly"]
}
target "license-validate" {
target = "license-validate"
output = ["type=cacheonly"]
}
target "license-update" {
target = "license-update"
output = ["."]
}
target "vendor-validate" {
inherits = ["_common"]
target = "vendor-validate"
output = ["type=cacheonly"]
}
target "vendor-update" {
inherits = ["_common"]
target = "vendor-update"
output = ["."]
}
target "test" {
inherits = ["_common"]
target = "test-coverage"
output = [outdir("./bin/coverage/unit")]
}
target "binary-with-coverage" {
inherits = ["_common"]
target = "binary"
args = {
BUILD_FLAGS = "-cover -covermode=atomic"
}
output = [outdir("./bin/build")]
platforms = ["local"]
}
target "binary" {
inherits = ["_common"]
target = "binary"
output = [outdir("./bin/build")]
platforms = ["local"]
}
target "binary-cross" {
inherits = ["binary"]
platforms = [
"darwin/amd64",
"darwin/arm64",
"linux/amd64",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/ppc64le",
"linux/riscv64",
"linux/s390x",
"windows/amd64",
"windows/arm64"
]
}
target "release" {
inherits = ["binary-cross"]
target = "release"
output = [outdir("./bin/release")]
}
target "docs-validate" {
inherits = ["_common"]
target = "docs-validate"
output = ["type=cacheonly"]
}
target "docs-update" {
inherits = ["_common"]
target = "docs-update"
output = ["./docs"]
}
target "image-cross" {
inherits = ["meta-helper", "binary-cross"]
output = ["type=image"]
}
================================================
FILE: docs/examples/provider.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func main() {
cmd := &cobra.Command{
Short: "Compose Provider Example",
Use: "demo",
}
cmd.AddCommand(composeCommand())
err := cmd.Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
type options struct {
db string
size int
}
func composeCommand() *cobra.Command {
c := &cobra.Command{
Use: "compose EVENT",
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
var options options
upCmd := &cobra.Command{
Use: "up",
Run: func(_ *cobra.Command, args []string) {
up(options, args)
},
Args: cobra.ExactArgs(1),
}
upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().IntVar(&options.size, "size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
downCmd := &cobra.Command{
Use: "down",
Run: down,
Args: cobra.ExactArgs(1),
}
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
return c
}
const lineSeparator = "\n"
func up(options options, args []string) {
servicename := args[0]
fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator)
for i := 0; i < options.size; i += 10 {
time.Sleep(1 * time.Second)
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i*100/options.size, lineSeparator)
}
fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator)
}
func down(_ *cobra.Command, _ []string) {
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
}
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "metadata",
Run: func(cmd *cobra.Command, _ []string) {
metadata(upCmd, downCmd)
},
Args: cobra.NoArgs,
}
}
func metadata(upCmd, downCmd *cobra.Command) {
metadata := ProviderMetadata{}
metadata.Description = "Manage services on AwesomeCloud"
metadata.Up = commandParameters(upCmd)
metadata.Down = commandParameters(downCmd)
jsonMetadata, err := json.Marshal(metadata)
if err != nil {
panic(err)
}
fmt.Println(string(jsonMetadata))
}
func commandParameters(cmd *cobra.Command) CommandMetadata {
cmdMetadata := CommandMetadata{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
Name: f.Name,
Description: f.Usage,
Required: isRequired,
Type: f.Value.Type(),
Default: f.DefValue,
})
})
return cmdMetadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []Metadata `json:"parameters"`
}
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
================================================
FILE: docs/extension.md
================================================
# About
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
application needs, which can interact with other services by relying on network(s). Docker Compose is designed
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
many other runtimes, typically cloud services or services natively provided by host.
The Compose extensibility model has been designed to extend the `service` support to runtimes accessible through
third-party tooling.
# Architecture
Compose extensibility relies on the `provider` attribute to select the actual binary responsible for managing
the resource(s) needed to run a service.
```yaml
database:
provider:
type: awesomecloud
options:
type: mysql
size: 256
name: myAwesomeCloudDB
```
`provider.type` tells Compose the binary to run, which can be either:
- Another Docker CLI plugin (typically, `model` to run `docker-model`)
- An executable in user's `PATH`
If `provider.type` doesn't resolve into any of those, Compose will report an error and interrupt the `up` command.
To be a valid Compose extension, provider command *MUST* accept a `compose` command (which can be hidden)
with subcommands `up` and `down`.
## Up lifecycle
To execute an application's `up` lifecycle, Compose executes the provider's `compose up` command, passing
the project name, service name, and additional options. The `provider.options` are translated
into command line flags. For example:
```console
awesomecloud compose --project-name up --type=mysql --size=256 "database"
```
> __Note:__ `project-name` _should_ be used by the provider to tag resources
> set for project, so that later execution with `down` subcommand releases
> all allocated resources set for the project.
## Communication with Compose
Providers can interact with Compose using `stdout` as a channel, sending JSON line delimited messages.
JSON messages MUST include a `type` and a `message` attribute.
```json
{ "type": "info", "message": "preparing mysql ..." }
```
`type` can be either:
- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI
- `error`: Lets the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
- `setenv`: Lets the plugin tell Compose how dependent services can access the created resource. See next section for further details.
- `debug`: Those messages could help debugging the provider, but are not rendered to the user by default. They are rendered when Compose is started with `--verbose` flag.
```mermaid
sequenceDiagram
Shell->>Compose: docker compose up
Compose->>Provider: compose up --project-name=xx --foo=bar "database"
Provider--)Compose: json { "info": "pulling 25%" }
Compose-)Shell: pulling 25%
Provider--)Compose: json { "info": "pulling 50%" }
Compose-)Shell: pulling 50%
Provider--)Compose: json { "info": "pulling 75%" }
Compose-)Shell: pulling 75%
Provider--)Compose: json { "setenv": "URL=http://cloud.com/abcd:1234" }
Compose-)Compose: set DATABASE_URL
Provider-)Compose: EOF (command complete) exit 0
Compose-)Shell: service started
```
## Connection to a service managed by a provider
A service in the Compose application can declare dependency on a service managed by an external provider:
```yaml
services:
app:
image: myapp
depends_on:
- database
database:
provider:
type: awesomecloud
```
When the provider command sends a `setenv` JSON message, Compose injects the specified variable into any dependent service,
automatically prefixing it with the service name. For example, if `awesomecloud compose up` returns:
```json
{"type": "setenv", "message": "URL=https://awesomecloud.com/db:1234"}
```
Then the `app` service, which depends on the service managed by the provider, will receive a `DATABASE_URL` environment variable injected
into its runtime environment.
> __Note:__ The `compose up` provider command _MUST_ be idempotent. If resource is already running, the command _MUST_ set
> the same environment variables to ensure consistent configuration of dependent services.
## Down lifecycle
`down` lifecycle is equivalent to `up` with the ` compose --project-name down ` command.
The provider is responsible for releasing all resources associated with the service.
## Provide metadata about options
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
The `metadata` subcommand takes no parameters and returns a JSON structure on the `stdout` channel that describes the parameters accepted by both the `up` and `down` commands, including whether each parameter is mandatory or optional.
```console
awesomecloud compose metadata
```
The expected JSON output format is:
```json
{
"description": "Manage services on AwesomeCloud",
"up": {
"parameters": [
{
"name": "type",
"description": "Database type (mysql, postgres, etc.)",
"required": true,
"type": "string"
},
{
"name": "size",
"description": "Database size in GB",
"required": false,
"type": "integer",
"default": "10"
},
{
"name": "name",
"description": "Name of the database to be created",
"required": true,
"type": "string"
}
]
},
"down": {
"parameters": [
{
"name": "name",
"description": "Name of the database to be removed",
"required": true,
"type": "string"
}
]
}
}
```
The top elements are:
- `description`: Human-readable description of the provider
- `up`: Object describing the parameters accepted by the `up` command
- `down`: Object describing the parameters accepted by the `down` command
And for each command parameter, you should include the following properties:
- `name`: The parameter name (without `--` prefix)
- `description`: Human-readable description of the parameter
- `required`: Boolean indicating if the parameter is mandatory
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
- `default`: Default value (optional, only for non-required parameters)
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
This metadata allows Compose and other tools to understand the provider's interface and provide better user experience, such as validation, auto-completion, and documentation generation.
## Examples
See [example](examples/provider.go) for illustration on implementing this API in a command line
================================================
FILE: docs/reference/compose.md
================================================
# docker compose
```text
docker compose [-f ...] [options] [COMMAND] [ARGS...]
```
Define and run multi-container applications with Docker
### Subcommands
| Name | Description |
|:--------------------------------|:----------------------------------------------------------------------------------------|
| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container |
| [`bridge`](compose_bridge.md) | Convert compose files into another model |
| [`build`](compose_build.md) | Build or rebuild services |
| [`commit`](compose_commit.md) | Create a new image from a service container's changes |
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem |
| [`create`](compose_create.md) | Creates containers for a service |
| [`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 |
| [`ls`](compose_ls.md) | List running compose projects |
| [`pause`](compose_pause.md) | Pause services |
| [`port`](compose_port.md) | Print the public port for a port binding |
| [`ps`](compose_ps.md) | List containers |
| [`publish`](compose_publish.md) | Publish compose application |
| [`pull`](compose_pull.md) | Pull service images |
| [`push`](compose_push.md) | Push service images |
| [`restart`](compose_restart.md) | Restart service containers |
| [`rm`](compose_rm.md) | Removes stopped service containers |
| [`run`](compose_run.md) | Run a one-off command on a service |
| [`scale`](compose_scale.md) | Scale services |
| [`start`](compose_start.md) | Start services |
| [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics |
| [`stop`](compose_stop.md) | Stop services |
| [`top`](compose_top.md) | Display the running processes |
| [`unpause`](compose_unpause.md) | Unpause services |
| [`up`](compose_up.md) | Create and start containers |
| [`version`](compose_version.md) | Show the Docker Compose version information |
| [`volumes`](compose_volumes.md) | List volumes |
| [`wait`](compose_wait.md) | Block until containers of all (or specified) services stop. |
| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated |
### Options
| Name | Type | Default | Description |
|:-----------------------|:--------------|:--------|:----------------------------------------------------------------------------------------------------|
| `--all-resources` | `bool` | | Include all resources, even those not used by services |
| `--ansi` | `string` | `auto` | Control when to print ANSI control characters ("never"\|"always"\|"auto") |
| `--compatibility` | `bool` | | Run compose in backward compatibility mode |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--env-file` | `stringArray` | | Specify an alternate environment file |
| `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--progress` | `string` | | Set type of progress output (auto, tty, plain, json, quiet) |
| `--project-directory` | `string` | | Specify an alternate working directory
(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |
## Examples
### Use `-f` to specify the name and path of one or more Compose files
Use the `-f` flag to specify the location of a Compose [configuration file](/reference/compose-file/).
#### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
configuration. Compose builds the configuration in the order you supply the files. Subsequent files override and add
to their predecessors.
For example, consider this command line:
```console
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db
```
The `compose.yaml` file might specify a `webapp` service.
```yaml
services:
webapp:
image: examples/web
ports:
- "8000:8000"
volumes:
- "/data"
```
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration.
```yaml
services:
webapp:
build: .
environment:
- DEBUG=1
```
When you use multiple Compose files, all paths in the files are relative to the first configuration file specified
with `-f`. You can use the `--project-directory` option to override this base path.
Use a `-f` with `-` (dash) as the filename to read the configuration from stdin. When stdin is used all paths in the
configuration are relative to the current working directory.
The `-f` flag is optional. If you don’t provide this flag on the command line, Compose traverses the working directory
and its parent directories looking for a `compose.yaml` or `docker-compose.yaml` file.
#### Specifying a path to a single Compose file
You can use the `-f` flag to specify a path to a Compose file that is not located in the current directory, either
from the command line or by setting up a `COMPOSE_FILE` environment variable in your shell or in an environment file.
For an example of using the `-f` option at the command line, suppose you are running the Compose Rails sample, and
have a `compose.yaml` file in a directory called `sandbox/rails`. You can use a command like `docker compose pull` to
get the postgres image for the db service from anywhere by using the `-f` flag as follows:
```console
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
```
#### Using an OCI published artifact
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
This allows you to distribute and version your Compose configurations as OCI artifacts.
To use a Compose file from an OCI registry:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
```
You can also combine OCI artifacts with local files:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
```
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
`docker compose publish` command.
#### Using a git repository
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
Using HTTPS:
```console
$ docker compose -f https://github.com/user/repo.git up
```
Using SSH:
```console
$ docker compose -f git@github.com:user/repo.git up
```
You can specify a specific branch, tag, or commit:
```console
$ docker compose -f https://github.com/user/repo.git@main up
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
$ docker compose -f https://github.com/user/repo.git@abc123 up
```
You can also specify a subdirectory within the repository:
```console
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
```
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
git resources with local files:
```console
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
```
### Use `-p` to specify a project name
Each configuration has a project name. Compose sets the project name using
the following mechanisms, in order of precedence:
- The `-p` command line flag
- The `COMPOSE_PROJECT_NAME` environment variable
- The top level `name:` variable from the config file (or the last `name:`
from a series of config files specified using `-f`)
- The `basename` of the project directory containing the config file (or
containing the first config file specified using `-f`)
- The `basename` of the current directory if no config file is specified
Project names must contain only lowercase letters, decimal digits, dashes,
and underscores, and must begin with a lowercase letter or decimal digit. If
the `basename` of the project directory or current directory violates this
constraint, you must use one of the other mechanisms.
```console
$ docker compose -p my_project ps -a
NAME SERVICE STATUS PORTS
my_project_demo_1 demo running
$ docker compose -p my_project logs
demo_1 | PING localhost (127.0.0.1): 56 data bytes
demo_1 | 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.095 ms
```
### Use profiles to enable optional services
Use `--profile` to specify one or more active profiles
Calling `docker compose --profile frontend up` starts the services with the profile `frontend` and services
without any specified profiles.
You can also enable multiple profiles, e.g. with `docker compose --profile frontend --profile debug up` the profiles `frontend` and `debug` is enabled.
Profiles can also be set by `COMPOSE_PROFILES` environment variable.
### Configuring parallelism
Use `--parallel` to specify the maximum level of parallelism for concurrent engine calls.
Calling `docker compose --parallel 1 pull` pulls the pullable images defined in the Compose file
one at a time. This can also be used to control build concurrency.
Parallelism can also be set by the `COMPOSE_PARALLEL_LIMIT` environment variable.
### Set up environment variables
You can set environment variables for various docker compose options, including the `-f`, `-p` and `--profiles` flags.
Setting the `COMPOSE_FILE` environment variable is equivalent to passing the `-f` flag,
`COMPOSE_PROJECT_NAME` environment variable does the same as the `-p` flag,
`COMPOSE_PROFILES` environment variable is equivalent to the `--profiles` flag
and `COMPOSE_PARALLEL_LIMIT` does the same as the `--parallel` flag.
If flags are explicitly set on the command line, the associated environment variable is ignored.
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` stops docker compose from detecting orphaned
containers for the project.
Setting the `COMPOSE_MENU` environment variable to `false` disables the helper menu when running `docker compose up`
in attached mode. Alternatively, you can also run `docker compose up --menu=false` to disable the helper menu.
### Use Dry Run mode to test your command
Use `--dry-run` flag to test a command without changing your application stack state.
Dry Run mode shows you all the steps Compose applies when executing a command, for example:
```console
$ docker compose --dry-run up --build -d
[+] Pulling 1/1
✔ DRY-RUN MODE - db Pulled 0.9s
[+] Running 10/8
✔ DRY-RUN MODE - build service backend 0.0s
✔ DRY-RUN MODE - ==> ==> writing image dryRun-754a08ddf8bcb1cf22f310f09206dd783d42f7dd 0.0s
✔ DRY-RUN MODE - ==> ==> naming to nginx-golang-mysql-backend 0.0s
✔ DRY-RUN MODE - Network nginx-golang-mysql_default Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Healthy 0.5s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Started 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Started Started
```
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
================================================
FILE: docs/reference/compose_alpha.md
================================================
# docker compose alpha
Experimental commands
### Subcommands
| Name | Description |
|:----------------------------------|:-----------------------------------------------------------------------------------------------------|
| [`viz`](compose_alpha_viz.md) | EXPERIMENTAL - Generate a graphviz graph from your compose file |
| [`watch`](compose_alpha_watch.md) | EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated |
### Options
| Name | Type | Default | Description |
|:------------|:-----|:--------|:--------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
================================================
FILE: docs/reference/compose_alpha_dry-run.md
================================================
# docker compose alpha dry-run
Dry run command allows you to test a command without applying changes
================================================
FILE: docs/reference/compose_alpha_generate.md
================================================
# docker compose alpha generate
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 |
================================================
FILE: docs/reference/compose_alpha_publish.md
================================================
# docker compose alpha publish
Publish compose application
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts |
================================================
FILE: docs/reference/compose_alpha_scale.md
================================================
# docker compose alpha scale
Scale services
### Options
| Name | Type | Default | Description |
|:------------|:-----|:--------|:--------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--no-deps` | | | Don't start linked services |
================================================
FILE: docs/reference/compose_alpha_viz.md
================================================
# docker compose alpha viz
EXPERIMENTAL - Generate a graphviz graph from your compose file
### Options
| Name | Type | Default | Description |
|:---------------------|:-------|:--------|:---------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--image` | `bool` | | Include service's image name in output graph |
| `--indentation-size` | `int` | `1` | Number of tabs or spaces to use for indentation |
| `--networks` | `bool` | | Include service's attached networks in output graph |
| `--ports` | `bool` | | Include service's exposed ports in output graph |
| `--spaces` | `bool` | | If given, space character ' ' will be used to indent,
otherwise tab character '\t' will be used |
================================================
FILE: docs/reference/compose_alpha_watch.md
================================================
# docker compose alpha watch
Watch build context for service and rebuild/refresh containers when files are updated
### Options
| Name | Type | Default | Description |
|:------------|:-----|:--------|:----------------------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--no-up` | | | Do not build & start services before watching |
| `--quiet` | | | hide build output |
================================================
FILE: docs/reference/compose_attach.md
================================================
# docker compose attach
Attach local standard input, output, and error streams to a service's running container
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:----------------------------------------------------------|
| `--detach-keys` | `string` | | Override the key sequence for detaching from a container. |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `--no-stdin` | `bool` | | Do not attach STDIN |
| `--sig-proxy` | `bool` | `true` | Proxy all received signals to the process |
================================================
FILE: docs/reference/compose_bridge.md
================================================
# docker compose bridge
Convert compose files into another model
### Subcommands
| Name | Description |
|:-------------------------------------------------------|:-----------------------------------------------------------------------------|
| [`convert`](compose_bridge_convert.md) | Convert compose files to Kubernetes manifests, Helm charts, or another model |
| [`transformations`](compose_bridge_transformations.md) | Manage transformation images |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
================================================
FILE: docs/reference/compose_bridge_convert.md
================================================
# docker compose bridge convert
Convert compose files to Kubernetes manifests, Helm charts, or another model
### Options
| Name | Type | Default | Description |
|:-------------------------|:--------------|:--------|:-------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-o`, `--output` | `string` | `out` | The output directory for the Kubernetes resources |
| `--templates` | `string` | | Directory containing transformation templates |
| `-t`, `--transformation` | `stringArray` | | Transformation to apply to compose model (default: docker/compose-bridge-kubernetes) |
================================================
FILE: docs/reference/compose_bridge_transformations.md
================================================
# docker compose bridge transformations
Manage transformation images
### Subcommands
| Name | Description |
|:-----------------------------------------------------|:-------------------------------|
| [`create`](compose_bridge_transformations_create.md) | Create a new transformation |
| [`list`](compose_bridge_transformations_list.md) | List available transformations |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
================================================
FILE: docs/reference/compose_bridge_transformations_create.md
================================================
# docker compose bridge transformations create
Create a new transformation
### Options
| Name | Type | Default | Description |
|:---------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--from` | `string` | | Existing transformation to copy (default: docker/compose-bridge-kubernetes) |
================================================
FILE: docs/reference/compose_bridge_transformations_list.md
================================================
# docker compose bridge transformations list
List available transformations
### Aliases
`docker compose bridge transformations list`, `docker compose bridge transformations ls`
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | `bool` | | Only display transformer names |
================================================
FILE: docs/reference/compose_build.md
================================================
# docker compose build
Services are built once and then tagged, by default as `project-service`.
If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it.
### Options
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
| `--build-arg` | `stringArray` | | Set build-time variables for services |
| `--builder` | `string` | | Set builder to use |
| `--check` | `bool` | | Check build configuration |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
| `--no-cache` | `bool` | | Do not use cache when building the image |
| `--print` | `bool` | | Print equivalent bake file |
| `--provenance` | `string` | | Add a provenance attestation |
| `--pull` | `bool` | | Always attempt to pull a newer version of the image |
| `--push` | `bool` | | Push service images |
| `-q`, `--quiet` | `bool` | | Suppress the build output |
| `--sbom` | `string` | | Add a SBOM attestation |
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
| `--with-dependencies` | `bool` | | Also build dependencies (transitively) |
## Description
Services are built once and then tagged, by default as `project-service`.
If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it.
================================================
FILE: docs/reference/compose_commit.md
================================================
# docker compose commit
Create a new image from a service container's changes
### Options
| Name | Type | Default | Description |
|:------------------|:---------|:--------|:-----------------------------------------------------------|
| `-a`, `--author` | `string` | | Author (e.g., "John Hannibal Smith ") |
| `-c`, `--change` | `list` | | Apply Dockerfile instruction to the created image |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `-m`, `--message` | `string` | | Commit message |
| `-p`, `--pause` | `bool` | `true` | Pause container during commit |
================================================
FILE: docs/reference/compose_config.md
================================================
# docker compose convert
`docker compose config` renders the actual data model to be applied on the Docker Engine.
It merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format.
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--environment` | `bool` | | Print environment used for interpolation. |
| `--format` | `string` | | Format the output. Values: [yaml \| json] |
| `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, one per line. |
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
| `--models` | `bool` | | Print the model names, one per line. |
| `--networks` | `bool` | | Print the network names, one per line. |
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |
| `--no-normalize` | `bool` | | Don't normalize compose model |
| `--no-path-resolution` | `bool` | | Don't resolve file paths |
| `-o`, `--output` | `string` | | Save to file (default to stdout) |
| `--profiles` | `bool` | | Print the profile names, one per line. |
| `-q`, `--quiet` | `bool` | | Only validate the configuration, don't print anything |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
| `--services` | `bool` | | Print the service names, one per line. |
| `--variables` | `bool` | | Print model variables and default values. |
| `--volumes` | `bool` | | Print the volume names, one per line. |
## Description
`docker compose config` renders the actual data model to be applied on the Docker Engine.
It merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format.
================================================
FILE: docs/reference/compose_cp.md
================================================
# docker compose cp
Copy files/folders between a service container and the local filesystem
### Options
| 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 |
| `--index` | `int` | `0` | Index of the container if service has multiple replicas |
================================================
FILE: docs/reference/compose_create.md
================================================
# docker compose create
Creates containers for a service
### Options
| Name | Type | Default | Description |
|:-------------------|:--------------|:---------|:----------------------------------------------------------------------------------------------|
| `--build` | `bool` | | Build images before starting containers |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--force-recreate` | `bool` | | Recreate containers even if their configuration and image haven't changed |
| `--no-build` | `bool` | | Don't build an image, even if it's policy |
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never"\|"build") |
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
================================================
FILE: docs/reference/compose_down.md
================================================
# docker compose down
Stops containers and removes containers, networks, volumes, and images created by `up`.
By default, the only things removed are:
- Containers for services defined in the Compose file.
- Networks defined in the networks section of the Compose file.
- The default network, if one is used.
Networks and volumes defined as external are never removed.
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
### Options
| Name | Type | Default | Description |
|:-------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
| `-v`, `--volumes` | `bool` | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers |
## Description
Stops containers and removes containers, networks, volumes, and images created by `up`.
By default, the only things removed are:
- Containers for services defined in the Compose file.
- Networks defined in the networks section of the Compose file.
- The default network, if one is used.
Networks and volumes defined as external are never removed.
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
================================================
FILE: docs/reference/compose_events.md
================================================
# docker compose events
Stream container events for every container in the project.
With the `--json` flag, a json object is printed one per line with the format:
```json
{
"time": "2015-11-20T18:01:03.615550",
"type": "container",
"action": "create",
"id": "213cf7...5fc39a",
"service": "web",
"attributes": {
"name": "application_web_1",
"image": "alpine:edge"
}
}
```
The events that can be received using this can be seen [here](/reference/cli/docker/system/events/#object-types).
### Options
| Name | Type | Default | Description |
|:------------|:---------|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--json` | `bool` | | Output events as a stream of json objects |
| `--since` | `string` | | Show all events created since timestamp |
| `--until` | `string` | | Stream events until this timestamp |
## Description
Stream container events for every container in the project.
With the `--json` flag, a json object is printed one per line with the format:
```json
{
"time": "2015-11-20T18:01:03.615550",
"type": "container",
"action": "create",
"id": "213cf7...5fc39a",
"service": "web",
"attributes": {
"name": "application_web_1",
"image": "alpine:edge"
}
}
```
The events that can be received using this can be seen [here](https://docs.docker.com/reference/cli/docker/system/events/#object-types).
================================================
FILE: docs/reference/compose_exec.md
================================================
# docker compose exec
This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
### Options
| Name | Type | Default | Description |
|:------------------|:--------------|:--------|:---------------------------------------------------------------------------------|
| `-d`, `--detach` | `bool` | | Detached mode: Run command in the background |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `0` | Index of the container if service has multiple replicas |
| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. |
| `--privileged` | `bool` | | Give extended privileges to the process |
| `-u`, `--user` | `string` | | Run the command as this user |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command |
## Description
This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
================================================
FILE: docs/reference/compose_export.md
================================================
# docker compose export
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 |
================================================
FILE: docs/reference/compose_images.md
================================================
# docker compose images
List images used by the created containers
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | `bool` | | Only display IDs |
================================================
FILE: docs/reference/compose_kill.md
================================================
# docker compose kill
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the signal can be passed, for example:
```console
$ docker compose kill -s SIGINT
```
### Options
| Name | Type | Default | Description |
|:-------------------|:---------|:----------|:---------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `-s`, `--signal` | `string` | `SIGKILL` | SIGNAL to send to the container |
## Description
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the signal can be passed, for example:
```console
$ docker compose kill -s SIGINT
```
================================================
FILE: docs/reference/compose_logs.md
================================================
# docker compose logs
Displays log output from services
### Options
| Name | Type | Default | Description |
|:---------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--follow` | `bool` | | Follow log output |
| `--index` | `int` | `0` | index of the container if service has multiple replicas |
| `--no-color` | `bool` | | Produce monochrome output |
| `--no-log-prefix` | `bool` | | Don't print prefix in logs |
| `--since` | `string` | | Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) |
| `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs for each container |
| `-t`, `--timestamps` | `bool` | | Show timestamps |
| `--until` | `string` | | Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) |
## Description
Displays log output from services
================================================
FILE: docs/reference/compose_ls.md
================================================
# docker compose ls
Lists running Compose projects
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------|
| `-a`, `--all` | `bool` | | Show all stopped Compose projects |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--filter` | `filter` | | Filter output based on conditions provided |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | `bool` | | Only display project names |
## Description
Lists running Compose projects
================================================
FILE: docs/reference/compose_pause.md
================================================
# docker compose pause
Pauses running containers of a service. They can be unpaused with `docker compose unpause`.
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
## Description
Pauses running containers of a service. They can be unpaused with `docker compose unpause`.
================================================
FILE: docs/reference/compose_port.md
================================================
# docker compose port
Prints the public port for a port binding
### 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 |
| `--protocol` | `string` | `tcp` | tcp or udp |
## Description
Prints the public port for a port binding
================================================
FILE: docs/reference/compose_ps.md
================================================
# docker compose ps
Lists containers for a Compose project, with current status and exposed ports.
```console
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
```
By default, only running containers are shown. `--all` flag can be used to include stopped containers.
```console
$ docker compose ps --all
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
example-bar-1 alpine "/entrypoint.…" bar 4 seconds ago exited (0)
```
### Options
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-a`, `--all` | `bool` | | Show all stopped containers (including those created by the run command) |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status) |
| [`--format`](#format) | `string` | `table` | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-trunc` | `bool` | | Don't truncate output |
| `--orphans` | `bool` | `true` | Include orphaned services (not declared by project) |
| `-q`, `--quiet` | `bool` | | Only display IDs |
| `--services` | `bool` | | Display services |
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
## Description
Lists containers for a Compose project, with current status and exposed ports.
```console
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
```
By default, only running containers are shown. `--all` flag can be used to include stopped containers.
```console
$ docker compose ps --all
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
example-bar-1 alpine "/entrypoint.…" bar 4 seconds ago exited (0)
```
## Examples
### Format the output (--format)
By default, the `docker compose ps` command uses a table ("pretty") format to
show the containers. The `--format` flag allows you to specify alternative
presentations for the output. Currently, supported options are `pretty` (default),
and `json`, which outputs information about the containers as a JSON array:
```console
$ docker compose ps --format json
[{"ID":"1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a","Name":"example-bar-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"bar","State":"exited","Health":"","ExitCode":0,"Publishers":null},{"ID":"f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0","Name":"example-foo-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"foo","State":"running","Health":"","ExitCode":0,"Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"}]}]
```
The JSON output allows you to use the information in other tools for further
processing, for example, using the [`jq` utility](https://stedolan.github.io/jq/)
to pretty-print the JSON:
```console
$ docker compose ps --format json | jq .
[
{
"ID": "1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a",
"Name": "example-bar-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "bar",
"State": "exited",
"Health": "",
"ExitCode": 0,
"Publishers": null
},
{
"ID": "f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0",
"Name": "example-foo-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "foo",
"State": "running",
"Health": "",
"ExitCode": 0,
"Publishers": [
{
"URL": "0.0.0.0",
"TargetPort": 80,
"PublishedPort": 8080,
"Protocol": "tcp"
}
]
}
]
```
### Filter containers by status (--status)
Use the `--status` flag to filter the list of containers by status. For example,
to show only containers that are running or only containers that have exited:
```console
$ docker compose ps --status=running
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
$ docker compose ps --status=exited
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-bar-1 alpine "/entrypoint.…" bar 4 seconds ago exited (0)
```
### Filter containers by status (--filter)
The [`--status` flag](#status) is a convenient shorthand for the `--filter status=`
flag. The example below is the equivalent to the example from the previous section,
this time using the `--filter` flag:
```console
$ docker compose ps --filter status=running
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
```
The `docker compose ps` command currently only supports the `--filter status=`
option, but additional filter options may be added in the future.
================================================
FILE: docs/reference/compose_publish.md
================================================
# docker compose publish
Publish compose application
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--app` | `bool` | | Published compose application (includes referenced images) |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts |
================================================
FILE: docs/reference/compose_pull.md
================================================
# docker compose pull
Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on those images
### Options
| Name | Type | Default | Description |
|:-------------------------|:---------|:--------|:-------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--ignore-buildable` | `bool` | | Ignore images that can be built |
| `--ignore-pull-failures` | `bool` | | Pull what it can and ignores images with pull failures |
| `--include-deps` | `bool` | | Also pull services declared as dependencies |
| `--policy` | `string` | | Apply pull policy ("missing"\|"always") |
| `-q`, `--quiet` | `bool` | | Pull without printing progress information |
## Description
Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on those images
## Examples
Consider the following `compose.yaml`:
```yaml
services:
db:
image: postgres
web:
build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
```
If you run `docker compose pull ServiceName` in the same directory as the `compose.yaml` file that defines the service,
Docker pulls the associated image. For example, to call the postgres image configured as the db service in our example,
you would run `docker compose pull db`.
```console
$ docker compose pull db
[+] Running 1/15
⠸ db Pulling 12.4s
⠿ 45b42c59be33 Already exists 0.0s
⠹ 40adec129f1a Downloading 3.374MB/4.178MB 9.3s
⠹ b4c431d00c78 Download complete 9.3s
⠹ 2696974e2815 Download complete 9.3s
⠹ 564b77596399 Downloading 5.622MB/7.965MB 9.3s
⠹ 5044045cf6f2 Downloading 216.7kB/391.1kB 9.3s
⠹ d736e67e6ac3 Waiting 9.3s
⠹ 390c1c9a5ae4 Waiting 9.3s
⠹ c0e62f172284 Waiting 9.3s
⠹ ebcdc659c5bf Waiting 9.3s
⠹ 29be22cb3acc Waiting 9.3s
⠹ f63c47038e66 Waiting 9.3s
⠹ 77a0c198cde5 Waiting 9.3s
⠹ c8752d5b785c Waiting 9.3s
```
`docker compose pull` tries to pull image for services with a build section. If pull fails, it lets you know this service image must be built. You can skip this by setting `--ignore-buildable` flag.
================================================
FILE: docs/reference/compose_push.md
================================================
# docker compose push
Pushes images for services to their respective registry/repository.
The following assumptions are made:
- You are pushing an image you have built locally
- You have access to the build key
Examples
```yaml
services:
service1:
build: .
image: localhost:5000/yourimage ## goes to local registry
service2:
build: .
image: your-dockerid/yourimage ## goes to your repository on Docker Hub
```
### Options
| Name | Type | Default | Description |
|:-------------------------|:-------|:--------|:-------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--ignore-push-failures` | `bool` | | Push what it can and ignores images with push failures |
| `--include-deps` | `bool` | | Also push images of services declared as dependencies |
| `-q`, `--quiet` | `bool` | | Push without printing progress information |
## Description
Pushes images for services to their respective registry/repository.
The following assumptions are made:
- You are pushing an image you have built locally
- You have access to the build key
Examples
```yaml
services:
service1:
build: .
image: localhost:5000/yourimage ## goes to local registry
service2:
build: .
image: your-dockerid/yourimage ## goes to your repository on Docker Hub
```
================================================
FILE: docs/reference/compose_restart.md
================================================
# docker compose restart
Restarts all stopped and running services, or the specified services only.
If you make changes to your `compose.yml` configuration, these changes are not reflected
after running this command. For example, changes to environment variables (which are added
after a container is built, but before the container's command is executed) are not updated
after restarting.
If you are looking to configure a service's restart policy, refer to
[restart](https://github.com/compose-spec/compose-spec/blob/main/spec.md#restart)
or [restart_policy](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#restart_policy).
### Options
| Name | Type | Default | Description |
|:------------------|:-------|:--------|:--------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--no-deps` | `bool` | | Don't restart dependent services |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
## Description
Restarts all stopped and running services, or the specified services only.
If you make changes to your `compose.yml` configuration, these changes are not reflected
after running this command. For example, changes to environment variables (which are added
after a container is built, but before the container's command is executed) are not updated
after restarting.
If you are looking to configure a service's restart policy, refer to
[restart](https://github.com/compose-spec/compose-spec/blob/main/spec.md#restart)
or [restart_policy](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#restart_policy).
================================================
FILE: docs/reference/compose_rm.md
================================================
# docker compose rm
Removes stopped service containers.
By default, anonymous volumes attached to containers are not removed. You can override this with `-v`. To list all
volumes, use `docker volume ls`.
Any data which is not in a volume is lost.
Running the command with no options also removes one-off containers created by `docker compose run`:
```console
$ docker compose rm
Going to remove djangoquickstart_web_run_1
Are you sure? [yN] y
Removing djangoquickstart_web_run_1 ... done
```
### Options
| Name | Type | Default | Description |
|:------------------|:-------|:--------|:----------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--force` | `bool` | | Don't ask to confirm removal |
| `-s`, `--stop` | `bool` | | Stop the containers, if required, before removing |
| `-v`, `--volumes` | `bool` | | Remove any anonymous volumes attached to containers |
## Description
Removes stopped service containers.
By default, anonymous volumes attached to containers are not removed. You can override this with `-v`. To list all
volumes, use `docker volume ls`.
Any data which is not in a volume is lost.
Running the command with no options also removes one-off containers created by `docker compose run`:
```console
$ docker compose rm
Going to remove djangoquickstart_web_run_1
Are you sure? [yN] y
Removing djangoquickstart_web_run_1 ... done
```
================================================
FILE: docs/reference/compose_run.md
================================================
# docker compose run
Runs a one-time command against a service.
The following command starts the `web` service and runs `bash` as its command:
```console
$ docker compose run web bash
```
Commands you use with run start in new containers with configuration defined by that of the service,
including volumes, links, and other details. However, there are two important differences:
First, the command passed by `run` overrides the command defined in the service configuration. For example, if the
`web` service configuration is started with `bash`, then `docker compose run web python app.py` overrides it with
`python app.py`.
The second difference is that the `docker compose run` command does not create any of the ports specified in the
service configuration. This prevents port collisions with already-open ports. If you do want the service’s ports
to be created and mapped to the host, specify the `--service-ports`
```console
$ docker compose run --service-ports web python manage.py shell
```
Alternatively, manual port mapping can be specified with the `--publish` or `-p` options, just as when using docker run:
```console
$ docker compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell
```
If you start a service configured with links, the run command first checks to see if the linked service is running
and starts the service if it is stopped. Once all the linked services are running, the run executes the command you
passed it. For example, you could run:
```console
$ docker compose run db psql -h db -U docker
```
This opens an interactive PostgreSQL shell for the linked `db` container.
If you do not want the run command to start linked containers, use the `--no-deps` flag:
```console
$ docker compose run --no-deps web python manage.py shell
```
If you want to remove the container after running while overriding the container’s restart policy, use the `--rm` flag:
```console
$ docker compose run --rm web python manage.py db upgrade
```
This runs a database upgrade script, and removes the container when finished running, even if a restart policy is
specified in the service configuration.
### Options
| Name | Type | Default | Description |
|:------------------------|:--------------|:---------|:---------------------------------------------------------------------------------|
| `--build` | `bool` | | Build image before starting container |
| `--cap-add` | `list` | | Add Linux capabilities |
| `--cap-drop` | `list` | | Drop Linux capabilities |
| `-d`, `--detach` | `bool` | | Run container in background and print container ID |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--entrypoint` | `string` | | Override the entrypoint of the image |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--env-from-file` | `stringArray` | | Set environment variables from file |
| `-i`, `--interactive` | `bool` | `true` | Keep STDIN open even if not attached |
| `-l`, `--label` | `stringArray` | | Add or override a label |
| `--name` | `string` | | Assign a name to the container |
| `-T`, `--no-TTY` | `bool` | `true` | Disable pseudo-TTY allocation (default: auto-detected) |
| `--no-deps` | `bool` | | Don't start linked services |
| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT |
| `--quiet-build` | `bool` | | Suppress progress output from the build process |
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--rm` | `bool` | | Automatically remove the container when it exits |
| `-P`, `--service-ports` | `bool` | | Run command with all service's ports enabled and mapped to the host |
| `--use-aliases` | `bool` | | Use the service's network useAliases in the network(s) the container connects to |
| `-u`, `--user` | `string` | | Run as specified username or uid |
| `-v`, `--volume` | `stringArray` | | Bind mount a volume |
| `-w`, `--workdir` | `string` | | Working directory inside the container |
## Description
Runs a one-time command against a service.
The following command starts the `web` service and runs `bash` as its command:
```console
$ docker compose run web bash
```
Commands you use with run start in new containers with configuration defined by that of the service,
including volumes, links, and other details. However, there are two important differences:
First, the command passed by `run` overrides the command defined in the service configuration. For example, if the
`web` service configuration is started with `bash`, then `docker compose run web python app.py` overrides it with
`python app.py`.
The second difference is that the `docker compose run` command does not create any of the ports specified in the
service configuration. This prevents port collisions with already-open ports. If you do want the service’s ports
to be created and mapped to the host, specify the `--service-ports`
```console
$ docker compose run --service-ports web python manage.py shell
```
Alternatively, manual port mapping can be specified with the `--publish` or `-p` options, just as when using docker run:
```console
$ docker compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell
```
If you start a service configured with links, the run command first checks to see if the linked service is running
and starts the service if it is stopped. Once all the linked services are running, the run executes the command you
passed it. For example, you could run:
```console
$ docker compose run db psql -h db -U docker
```
This opens an interactive PostgreSQL shell for the linked `db` container.
If you do not want the run command to start linked containers, use the `--no-deps` flag:
```console
$ docker compose run --no-deps web python manage.py shell
```
If you want to remove the container after running while overriding the container’s restart policy, use the `--rm` flag:
```console
$ docker compose run --rm web python manage.py db upgrade
```
This runs a database upgrade script, and removes the container when finished running, even if a restart policy is
specified in the service configuration.
================================================
FILE: docs/reference/compose_scale.md
================================================
# docker compose scale
Scale services
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--no-deps` | `bool` | | Don't start linked services |
================================================
FILE: docs/reference/compose_start.md
================================================
# docker compose start
Starts existing containers for a service
### Options
| Name | Type | Default | Description |
|:-----------------|:-------|:--------|:---------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. |
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy |
## Description
Starts existing containers for a service
================================================
FILE: docs/reference/compose_stats.md
================================================
# docker compose stats
Display a live stream of container(s) resource usage statistics
### Options
| Name | Type | Default | Description |
|:--------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-a`, `--all` | `bool` | | Show all containers (default shows just running) |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates |
| `--no-stream` | `bool` | | Disable streaming stats and only pull the first result |
| `--no-trunc` | `bool` | | Do not truncate output |
================================================
FILE: docs/reference/compose_stop.md
================================================
# docker compose stop
Stops running containers without removing them. They can be started again with `docker compose start`.
### Options
| Name | Type | Default | Description |
|:------------------|:-------|:--------|:--------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
## Description
Stops running containers without removing them. They can be started again with `docker compose start`.
================================================
FILE: docs/reference/compose_top.md
================================================
# docker compose top
Displays the running processes
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
## Description
Displays the running processes
## Examples
```console
$ docker compose top
example_foo_1
UID PID PPID C STIME TTY TIME CMD
root 142353 142331 2 15:33 ? 00:00:00 ping localhost -c 5
```
================================================
FILE: docs/reference/compose_unpause.md
================================================
# docker compose unpause
Unpauses paused containers of a service
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
## Description
Unpauses paused containers of a service
================================================
FILE: docs/reference/compose_up.md
================================================
# docker compose up
Builds, (re)creates, starts, and attaches to containers for a service.
Unless they are already running, this command also starts any linked services.
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
One can optionally select a subset of services to attach to using `--attach` flag, or exclude some services using
`--no-attach` to prevent output to be flooded by some verbose services.
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
background and leaves them running.
If there are existing containers for a service, and the service’s configuration or image was changed after the
container’s creation, `docker compose up` picks up the changes by stopping and recreating the containers
(preserving mounted volumes). To prevent Compose from picking up changes, use the `--no-recreate` flag.
If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag.
If the process encounters an error, the exit code for this command is `1`.
If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the containers are stopped, and the exit code is `0`.
### Options
| Name | Type | Default | Description |
|:-------------------------------|:--------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
| `--abort-on-container-exit` | `bool` | | Stops all containers if any container was stopped. Incompatible with -d |
| `--abort-on-container-failure` | `bool` | | Stops all containers if any container exited with failure. Incompatible with -d |
| `--always-recreate-deps` | `bool` | | Recreate dependent containers. Incompatible with --no-recreate. |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--attach-dependencies` | `bool` | | Automatically attach to log output of dependent services |
| `--build` | `bool` | | Build images before starting containers |
| `-d`, `--detach` | `bool` | | Detached mode: Run containers in the background |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--force-recreate` | `bool` | | Recreate containers even if their configuration and image haven't changed |
| `--menu` | `bool` | | Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var. |
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
| `--no-build` | `bool` | | Don't build an image, even if it's policy |
| `--no-color` | `bool` | | Produce monochrome output |
| `--no-deps` | `bool` | | Don't start linked services |
| `--no-log-prefix` | `bool` | | Don't print prefix in logs |
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--no-start` | `bool` | | Don't start the services after creating them |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `--quiet-build` | `bool` | | Suppress the build output |
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `-V`, `--renew-anon-volumes` | `bool` | | Recreate anonymous volumes instead of retrieving data from the previous containers |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
| `--timestamps` | `bool` | | Show timestamps |
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. |
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy |
| `-w`, `--watch` | `bool` | | Watch source code and rebuild/refresh containers when files are updated. |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
## Description
Builds, (re)creates, starts, and attaches to containers for a service.
Unless they are already running, this command also starts any linked services.
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
One can optionally select a subset of services to attach to using `--attach` flag, or exclude some services using
`--no-attach` to prevent output to be flooded by some verbose services.
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
background and leaves them running.
If there are existing containers for a service, and the service’s configuration or image was changed after the
container’s creation, `docker compose up` picks up the changes by stopping and recreating the containers
(preserving mounted volumes). To prevent Compose from picking up changes, use the `--no-recreate` flag.
If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag.
If the process encounters an error, the exit code for this command is `1`.
If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the containers are stopped, and the exit code is `0`.
================================================
FILE: docs/reference/compose_version.md
================================================
# docker compose version
Show the Docker Compose version information
### Options
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:---------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--format` | `string` | | Format the output. Values: [pretty \| json]. (Default: pretty) |
| `--short` | `bool` | | Shows only Compose's version number |
================================================
FILE: docs/reference/compose_volumes.md
================================================
# docker compose volumes
List volumes
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `-q`, `--quiet` | `bool` | | Only display volume names |
================================================
FILE: docs/reference/compose_wait.md
================================================
# docker compose wait
Block until containers of all (or specified) services stop.
### Options
| Name | Type | Default | Description |
|:-----------------|:-------|:--------|:---------------------------------------------|
| `--down-project` | `bool` | | Drops project when the first container stops |
| `--dry-run` | `bool` | | Execute command in dry run mode |
================================================
FILE: docs/reference/compose_watch.md
================================================
# docker compose watch
Watch build context for service and rebuild/refresh containers when files are updated
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:----------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--no-up` | `bool` | | Do not build & start services before watching |
| `--prune` | `bool` | `true` | Prune dangling images on rebuild |
| `--quiet` | `bool` | | hide build output |
================================================
FILE: docs/reference/docker_compose.yaml
================================================
command: docker compose
short: Docker Compose
long: Define and run multi-container applications with Docker
usage: docker compose
pname: docker
plink: docker.yaml
cname:
- docker compose attach
- docker compose bridge
- docker compose build
- docker compose commit
- docker compose config
- docker compose cp
- docker compose create
- docker compose down
- docker compose events
- docker compose exec
- docker compose export
- docker compose images
- docker compose kill
- docker compose logs
- docker compose ls
- docker compose pause
- docker compose port
- docker compose ps
- docker compose publish
- docker compose pull
- docker compose push
- docker compose restart
- docker compose rm
- docker compose run
- docker compose scale
- docker compose start
- docker compose stats
- docker compose stop
- docker compose top
- docker compose unpause
- docker compose up
- docker compose version
- docker compose volumes
- docker compose wait
- docker compose watch
clink:
- docker_compose_attach.yaml
- docker_compose_bridge.yaml
- docker_compose_build.yaml
- docker_compose_commit.yaml
- docker_compose_config.yaml
- docker_compose_cp.yaml
- docker_compose_create.yaml
- 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
- docker_compose_ls.yaml
- docker_compose_pause.yaml
- docker_compose_port.yaml
- docker_compose_ps.yaml
- docker_compose_publish.yaml
- docker_compose_pull.yaml
- docker_compose_push.yaml
- docker_compose_restart.yaml
- docker_compose_rm.yaml
- docker_compose_run.yaml
- docker_compose_scale.yaml
- docker_compose_start.yaml
- docker_compose_stats.yaml
- docker_compose_stop.yaml
- docker_compose_top.yaml
- docker_compose_unpause.yaml
- docker_compose_up.yaml
- docker_compose_version.yaml
- docker_compose_volumes.yaml
- docker_compose_wait.yaml
- docker_compose_watch.yaml
options:
- option: all-resources
value_type: bool
default_value: "false"
description: Include all resources, even those not used by services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: ansi
value_type: string
default_value: auto
description: |
Control when to print ANSI control characters ("never"|"always"|"auto")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compatibility
value_type: bool
default_value: "false"
description: Run compose in backward compatibility mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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
- option: env-file
value_type: stringArray
default_value: '[]'
description: Specify an alternate environment file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: file
shorthand: f
value_type: stringArray
default_value: '[]'
description: Compose configuration files
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: insecure-registry
value_type: stringArray
default_value: '[]'
description: |
Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-ansi
value_type: bool
default_value: "false"
description: Do not print ANSI control characters (DEPRECATED)
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: parallel
value_type: int
default_value: "-1"
description: Control max parallelism, -1 for unlimited
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: profile
value_type: stringArray
default_value: '[]'
description: Specify a profile to enable
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: progress
value_type: string
description: Set type of progress output (auto, tty, plain, json, quiet)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: project-directory
value_type: string
description: |-
Specify an alternate working directory
(default: the path of the, first specified, Compose file)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: project-name
shorthand: p
value_type: string
description: Project name
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: verbose
value_type: bool
default_value: "false"
description: Show more output
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: version
shorthand: v
value_type: bool
default_value: "false"
description: Show the Docker Compose version information
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: workdir
value_type: string
description: |-
DEPRECATED! USE --project-directory INSTEAD.
Specify an alternate working directory
(default: the path of the, first specified, Compose file)
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
examples: |-
### Use `-f` to specify the name and path of one or more Compose files
Use the `-f` flag to specify the location of a Compose [configuration file](/reference/compose-file/).
#### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
configuration. Compose builds the configuration in the order you supply the files. Subsequent files override and add
to their predecessors.
For example, consider this command line:
```console
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db
```
The `compose.yaml` file might specify a `webapp` service.
```yaml
services:
webapp:
image: examples/web
ports:
- "8000:8000"
volumes:
- "/data"
```
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration.
```yaml
services:
webapp:
build: .
environment:
- DEBUG=1
```
When you use multiple Compose files, all paths in the files are relative to the first configuration file specified
with `-f`. You can use the `--project-directory` option to override this base path.
Use a `-f` with `-` (dash) as the filename to read the configuration from stdin. When stdin is used all paths in the
configuration are relative to the current working directory.
The `-f` flag is optional. If you don’t provide this flag on the command line, Compose traverses the working directory
and its parent directories looking for a `compose.yaml` or `docker-compose.yaml` file.
#### Specifying a path to a single Compose file
You can use the `-f` flag to specify a path to a Compose file that is not located in the current directory, either
from the command line or by setting up a `COMPOSE_FILE` environment variable in your shell or in an environment file.
For an example of using the `-f` option at the command line, suppose you are running the Compose Rails sample, and
have a `compose.yaml` file in a directory called `sandbox/rails`. You can use a command like `docker compose pull` to
get the postgres image for the db service from anywhere by using the `-f` flag as follows:
```console
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
```
#### Using an OCI published artifact
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
This allows you to distribute and version your Compose configurations as OCI artifacts.
To use a Compose file from an OCI registry:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
```
You can also combine OCI artifacts with local files:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
```
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
`docker compose publish` command.
#### Using a git repository
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
Using HTTPS:
```console
$ docker compose -f https://github.com/user/repo.git up
```
Using SSH:
```console
$ docker compose -f git@github.com:user/repo.git up
```
You can specify a specific branch, tag, or commit:
```console
$ docker compose -f https://github.com/user/repo.git@main up
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
$ docker compose -f https://github.com/user/repo.git@abc123 up
```
You can also specify a subdirectory within the repository:
```console
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
```
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
git resources with local files:
```console
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
```
### Use `-p` to specify a project name
Each configuration has a project name. Compose sets the project name using
the following mechanisms, in order of precedence:
- The `-p` command line flag
- The `COMPOSE_PROJECT_NAME` environment variable
- The top level `name:` variable from the config file (or the last `name:`
from a series of config files specified using `-f`)
- The `basename` of the project directory containing the config file (or
containing the first config file specified using `-f`)
- The `basename` of the current directory if no config file is specified
Project names must contain only lowercase letters, decimal digits, dashes,
and underscores, and must begin with a lowercase letter or decimal digit. If
the `basename` of the project directory or current directory violates this
constraint, you must use one of the other mechanisms.
```console
$ docker compose -p my_project ps -a
NAME SERVICE STATUS PORTS
my_project_demo_1 demo running
$ docker compose -p my_project logs
demo_1 | PING localhost (127.0.0.1): 56 data bytes
demo_1 | 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.095 ms
```
### Use profiles to enable optional services
Use `--profile` to specify one or more active profiles
Calling `docker compose --profile frontend up` starts the services with the profile `frontend` and services
without any specified profiles.
You can also enable multiple profiles, e.g. with `docker compose --profile frontend --profile debug up` the profiles `frontend` and `debug` is enabled.
Profiles can also be set by `COMPOSE_PROFILES` environment variable.
### Configuring parallelism
Use `--parallel` to specify the maximum level of parallelism for concurrent engine calls.
Calling `docker compose --parallel 1 pull` pulls the pullable images defined in the Compose file
one at a time. This can also be used to control build concurrency.
Parallelism can also be set by the `COMPOSE_PARALLEL_LIMIT` environment variable.
### Set up environment variables
You can set environment variables for various docker compose options, including the `-f`, `-p` and `--profiles` flags.
Setting the `COMPOSE_FILE` environment variable is equivalent to passing the `-f` flag,
`COMPOSE_PROJECT_NAME` environment variable does the same as the `-p` flag,
`COMPOSE_PROFILES` environment variable is equivalent to the `--profiles` flag
and `COMPOSE_PARALLEL_LIMIT` does the same as the `--parallel` flag.
If flags are explicitly set on the command line, the associated environment variable is ignored.
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` stops docker compose from detecting orphaned
containers for the project.
Setting the `COMPOSE_MENU` environment variable to `false` disables the helper menu when running `docker compose up`
in attached mode. Alternatively, you can also run `docker compose up --menu=false` to disable the helper menu.
### Use Dry Run mode to test your command
Use `--dry-run` flag to test a command without changing your application stack state.
Dry Run mode shows you all the steps Compose applies when executing a command, for example:
```console
$ docker compose --dry-run up --build -d
[+] Pulling 1/1
✔ DRY-RUN MODE - db Pulled 0.9s
[+] Running 10/8
✔ DRY-RUN MODE - build service backend 0.0s
✔ DRY-RUN MODE - ==> ==> writing image dryRun-754a08ddf8bcb1cf22f310f09206dd783d42f7dd 0.0s
✔ DRY-RUN MODE - ==> ==> naming to nginx-golang-mysql-backend 0.0s
✔ DRY-RUN MODE - Network nginx-golang-mysql_default Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Healthy 0.5s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Started 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Started Started
```
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha.yaml
================================================
command: docker compose alpha
short: Experimental commands
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:
- 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: true
experimental: false
experimentalcli: true
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha_dry-run.yaml
================================================
command: docker compose alpha dry-run
short: |
EXPERIMENTAL - Dry run command allow you to test a command without applying changes
long: |
EXPERIMENTAL - Dry run command allow you to test a command without applying changes
usage: docker compose alpha dry-run -- [COMMAND...]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
deprecated: false
experimental: false
experimentalcli: true
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha_generate.yaml
================================================
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: true
experimental: false
experimentalcli: true
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha_publish.yaml
================================================
command: docker compose alpha publish
short: Publish compose application
long: Publish compose application
usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: app
value_type: bool
default_value: "false"
description: Published compose application (includes referenced images)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: insecure-registry
value_type: bool
default_value: "false"
description: Use insecure registry
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: oci-version
value_type: string
description: |
OCI image/artifact specification version (automatically determined by default)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
description: Pin image tags to digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: with-env
value_type: bool
default_value: "false"
description: Include environment variables in the published OCI artifact
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"
description: Assume "yes" as answer to all prompts
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: true
experimental: false
experimentalcli: true
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha_scale.yaml
================================================
command: docker compose alpha scale
short: Scale services
long: Scale services
usage: docker compose alpha scale [SERVICE=REPLICAS...]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: no-deps
value_type: bool
default_value: "false"
description: Don't start linked services.
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
================================================
FILE: docs/reference/docker_compose_alpha_viz.yaml
================================================
command: docker compose alpha viz
short: EXPERIMENTAL - Generate a graphviz graph from your compose file
long: EXPERIMENTAL - Generate a graphviz graph from your compose file
usage: docker compose alpha viz [OPTIONS]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: image
value_type: bool
default_value: "false"
description: Include service's image name in output graph
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: indentation-size
value_type: int
default_value: "1"
description: Number of tabs or spaces to use for indentation
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: networks
value_type: bool
default_value: "false"
description: Include service's attached networks in output graph
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: ports
value_type: bool
default_value: "false"
description: Include service's exposed ports in output graph
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: spaces
value_type: bool
default_value: "false"
description: |-
If given, space character ' ' will be used to indent,
otherwise tab character '\t' will be used
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: true
experimental: false
experimentalcli: true
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_alpha_watch.yaml
================================================
command: docker compose alpha watch
short: |
Watch build context for service and rebuild/refresh containers when files are updated
long: |
Watch build context for service and rebuild/refresh containers when files are updated
usage: docker compose alpha watch [SERVICE...]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: no-up
value_type: bool
default_value: "false"
description: Do not build & start services before watching
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
value_type: bool
default_value: "false"
description: hide build output
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
================================================
FILE: docs/reference/docker_compose_attach.yaml
================================================
command: docker compose attach
short: |
Attach local standard input, output, and error streams to a service's running container
long: |
Attach local standard input, output, and error streams to a service's running container
usage: docker compose attach [OPTIONS] SERVICE
pname: docker compose
plink: docker_compose.yaml
options:
- option: detach-keys
value_type: string
description: Override the key sequence for detaching from a container.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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: no-stdin
value_type: bool
default_value: "false"
description: Do not attach STDIN
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: sig-proxy
value_type: bool
default_value: "true"
description: Proxy all received signals to the process
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
================================================
FILE: docs/reference/docker_compose_bridge.yaml
================================================
command: docker compose bridge
short: Convert compose files into another model
long: Convert compose files into another model
pname: docker compose
plink: docker_compose.yaml
cname:
- docker compose bridge convert
- docker compose bridge transformations
clink:
- docker_compose_bridge_convert.yaml
- docker_compose_bridge_transformations.yaml
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
================================================
FILE: docs/reference/docker_compose_bridge_convert.yaml
================================================
command: docker compose bridge convert
short: |
Convert compose files to Kubernetes manifests, Helm charts, or another model
long: |
Convert compose files to Kubernetes manifests, Helm charts, or another model
usage: docker compose bridge convert
pname: docker compose bridge
plink: docker_compose_bridge.yaml
options:
- option: output
shorthand: o
value_type: string
default_value: out
description: The output directory for the Kubernetes resources
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: templates
value_type: string
description: Directory containing transformation templates
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: transformation
shorthand: t
value_type: stringArray
default_value: '[]'
description: |
Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)
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
================================================
FILE: docs/reference/docker_compose_bridge_transformations.yaml
================================================
command: docker compose bridge transformations
short: Manage transformation images
long: Manage transformation images
pname: docker compose bridge
plink: docker_compose_bridge.yaml
cname:
- docker compose bridge transformations create
- docker compose bridge transformations list
clink:
- docker_compose_bridge_transformations_create.yaml
- docker_compose_bridge_transformations_list.yaml
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
================================================
FILE: docs/reference/docker_compose_bridge_transformations_create.yaml
================================================
command: docker compose bridge transformations create
short: Create a new transformation
long: Create a new transformation
usage: docker compose bridge transformations create [OPTION] PATH
pname: docker compose bridge transformations
plink: docker_compose_bridge_transformations.yaml
options:
- option: from
shorthand: f
value_type: string
description: |
Existing transformation to copy (default: docker/compose-bridge-kubernetes)
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
================================================
FILE: docs/reference/docker_compose_bridge_transformations_list.yaml
================================================
command: docker compose bridge transformations list
aliases: docker compose bridge transformations list, docker compose bridge transformations ls
short: List available transformations
long: List available transformations
usage: docker compose bridge transformations list
pname: docker compose bridge transformations
plink: docker_compose_bridge_transformations.yaml
options:
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display transformer names
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
================================================
FILE: docs/reference/docker_compose_build.yaml
================================================
command: docker compose build
short: Build or rebuild services
long: |-
Services are built once and then tagged, by default as `project-service`.
If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it.
usage: docker compose build [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: build-arg
value_type: stringArray
default_value: '[]'
description: Set build-time variables for services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: builder
value_type: string
description: Set builder to use
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: check
value_type: bool
default_value: "false"
description: Check build configuration
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compress
value_type: bool
default_value: "true"
description: Compress the build context using gzip. DEPRECATED
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: force-rm
value_type: bool
default_value: "true"
description: Always remove intermediate containers. DEPRECATED
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: memory
shorthand: m
value_type: bytes
default_value: "0"
description: |
Set memory limit for the build container. Not supported by BuildKit.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-cache
value_type: bool
default_value: "false"
description: Do not use cache when building the image
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-rm
value_type: bool
default_value: "false"
description: |
Do not remove intermediate containers after a successful build. DEPRECATED
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: parallel
value_type: bool
default_value: "true"
description: Build images in parallel. DEPRECATED
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: print
value_type: bool
default_value: "false"
description: Print equivalent bake file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: progress
value_type: string
description: Set type of ui output (auto, tty, plain, json, quiet)
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: provenance
value_type: string
description: Add a provenance attestation
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pull
value_type: bool
default_value: "false"
description: Always attempt to pull a newer version of the image
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: push
value_type: bool
default_value: "false"
description: Push service images
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Suppress the build output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: sbom
value_type: string
description: Add a SBOM attestation
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: ssh
value_type: string
description: |
Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: with-dependencies
value_type: bool
default_value: "false"
description: Also build dependencies (transitively)
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
================================================
FILE: docs/reference/docker_compose_commit.yaml
================================================
command: docker compose commit
short: Create a new image from a service container's changes
long: Create a new image from a service container's changes
usage: docker compose commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]
pname: docker compose
plink: docker_compose.yaml
options:
- option: author
shorthand: a
value_type: string
description: Author (e.g., "John Hannibal Smith ")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: change
shorthand: c
value_type: list
description: Apply Dockerfile instruction to the created image
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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: message
shorthand: m
value_type: string
description: Commit message
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pause
shorthand: p
value_type: bool
default_value: "true"
description: Pause container during commit
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
================================================
FILE: docs/reference/docker_compose_config.yaml
================================================
command: docker compose config
short: Parse, resolve and render compose file in canonical format
long: |-
`docker compose config` renders the actual data model to be applied on the Docker Engine.
It merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format.
usage: docker compose config [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: environment
value_type: bool
default_value: "false"
description: Print environment used for interpolation.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: format
value_type: string
description: 'Format the output. Values: [yaml | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: hash
value_type: string
description: Print the service config hash, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: images
value_type: bool
default_value: "false"
description: Print the image names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: lock-image-digests
value_type: bool
default_value: "false"
description: Produces an override file with image digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: models
value_type: bool
default_value: "false"
description: Print the model names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: networks
value_type: bool
default_value: "false"
description: Print the network names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-consistency
value_type: bool
default_value: "false"
description: |
Don't check model consistency - warning: may produce invalid Compose output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-env-resolution
value_type: bool
default_value: "false"
description: Don't resolve service env files
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-interpolate
value_type: bool
default_value: "false"
description: Don't interpolate environment variables
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-normalize
value_type: bool
default_value: "false"
description: Don't normalize compose model
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-path-resolution
value_type: bool
default_value: "false"
description: Don't resolve file paths
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: output
shorthand: o
value_type: string
description: Save to file (default to stdout)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: profiles
value_type: bool
default_value: "false"
description: Print the profile names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only validate the configuration, don't print anything
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
description: Pin image tags to digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: services
value_type: bool
default_value: "false"
description: Print the service names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: variables
value_type: bool
default_value: "false"
description: Print model variables and default values.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: volumes
value_type: bool
default_value: "false"
description: Print the volume names, one per line.
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
================================================
FILE: docs/reference/docker_compose_convert.yaml
================================================
command: docker compose convert
aliases: docker compose convert, docker compose config
short: Converts the compose file to platform's canonical format
long: |-
`docker compose convert` renders the actual data model to be applied on the target platform. When used with the Docker engine,
it merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format.
To allow smooth migration from docker-compose, this subcommand declares alias `docker compose config`
usage: docker compose convert [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.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: hash
value_type: string
description: Print the service config hash, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: images
value_type: bool
default_value: "false"
description: Print the image names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-consistency
value_type: bool
default_value: "false"
description: |
Don't check model consistency - warning: may produce invalid Compose output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-interpolate
value_type: bool
default_value: "false"
description: Don't interpolate environment variables.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-normalize
value_type: bool
default_value: "false"
description: Don't normalize compose model.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: output
shorthand: o
value_type: string
description: Save to file (default to stdout)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: profiles
value_type: bool
default_value: "false"
description: Print the profile names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only validate the configuration, don't print anything.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
description: Pin image tags to digests.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: services
value_type: bool
default_value: "false"
description: Print the service names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: volumes
value_type: bool
default_value: "false"
description: Print the volume names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_cp.yaml
================================================
command: docker compose cp
short: Copy files/folders between a service container and the local filesystem
long: Copy files/folders between a service container and the local filesystem
usage: |-
docker compose cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH
pname: docker compose
plink: docker_compose.yaml
options:
- option: all
value_type: bool
default_value: "false"
description: Include containers created by the run command
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: archive
shorthand: a
value_type: bool
default_value: "false"
description: Archive mode (copy all uid/gid information)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: follow-link
shorthand: L
value_type: bool
default_value: "false"
description: Always follow symbol link in SRC_PATH
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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
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
================================================
FILE: docs/reference/docker_compose_create.yaml
================================================
command: docker compose create
short: Creates containers for a service
long: Creates containers for a service
usage: docker compose create [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: build
value_type: bool
default_value: "false"
description: Build images before starting containers
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: force-recreate
value_type: bool
default_value: "false"
description: |
Recreate containers even if their configuration and image haven't changed
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-build
value_type: bool
default_value: "false"
description: Don't build an image, even if it's policy
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-recreate
value_type: bool
default_value: "false"
description: |
If containers already exist, don't recreate them. Incompatible with --force-recreate.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pull
value_type: string
default_value: policy
description: Pull image before running ("always"|"missing"|"never"|"build")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"
description: Pull without printing progress information
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: scale
value_type: stringArray
default_value: '[]'
description: |
Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"
description: Assume "yes" as answer to all prompts and run non-interactively
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
================================================
FILE: docs/reference/docker_compose_down.yaml
================================================
command: docker compose down
short: Stop and remove containers, networks
long: |-
Stops containers and removes containers, networks, volumes, and images created by `up`.
By default, the only things removed are:
- Containers for services defined in the Compose file.
- Networks defined in the networks section of the Compose file.
- The default network, if one is used.
Networks and volumes defined as external are never removed.
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
usage: docker compose down [OPTIONS] [SERVICES]
pname: docker compose
plink: docker_compose.yaml
options:
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: rmi
value_type: string
description: |
Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: timeout
shorthand: t
value_type: int
default_value: "0"
description: Specify a shutdown timeout in seconds
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: volumes
shorthand: v
value_type: bool
default_value: "false"
description: |
Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers
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
================================================
FILE: docs/reference/docker_compose_events.yaml
================================================
command: docker compose events
short: Receive real time events from containers
long: |-
Stream container events for every container in the project.
With the `--json` flag, a json object is printed one per line with the format:
```json
{
"time": "2015-11-20T18:01:03.615550",
"type": "container",
"action": "create",
"id": "213cf7...5fc39a",
"service": "web",
"attributes": {
"name": "application_web_1",
"image": "alpine:edge"
}
}
```
The events that can be received using this can be seen [here](/reference/cli/docker/system/events/#object-types).
usage: docker compose events [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: json
value_type: bool
default_value: "false"
description: Output events as a stream of json objects
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: since
value_type: string
description: Show all events created since timestamp
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: until
value_type: string
description: Stream events until this timestamp
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
================================================
FILE: docs/reference/docker_compose_exec.yaml
================================================
command: docker compose exec
short: Execute a command in a running container
long: |-
This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
usage: docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: detach
shorthand: d
value_type: bool
default_value: "false"
description: 'Detached mode: Run command in the background'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: env
shorthand: e
value_type: stringArray
default_value: '[]'
description: Set environment variables
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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: interactive
shorthand: i
value_type: bool
default_value: "true"
description: Keep STDIN open even if not attached
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-tty
shorthand: T
value_type: bool
default_value: "true"
description: |
Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: privileged
value_type: bool
default_value: "false"
description: Give extended privileges to the process
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: tty
shorthand: t
value_type: bool
default_value: "true"
description: Allocate a pseudo-TTY
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: user
shorthand: u
value_type: string
description: Run the command as this user
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: workdir
shorthand: w
value_type: string
description: Path to workdir directory for this command
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
================================================
FILE: docs/reference/docker_compose_export.yaml
================================================
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
================================================
FILE: docs/reference/docker_compose_images.yaml
================================================
command: docker compose images
short: List images used by the created containers
long: List images used by the created containers
usage: docker compose images [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display IDs
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
================================================
FILE: docs/reference/docker_compose_kill.yaml
================================================
command: docker compose kill
short: Force stop service containers
long: |-
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the signal can be passed, for example:
```console
$ docker compose kill -s SIGINT
```
usage: docker compose kill [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: signal
shorthand: s
value_type: string
default_value: SIGKILL
description: SIGNAL to send to the container
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
================================================
FILE: docs/reference/docker_compose_logs.yaml
================================================
command: docker compose logs
short: View output from containers
long: Displays log output from services
usage: docker compose logs [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: follow
shorthand: f
value_type: bool
default_value: "false"
description: Follow log output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- 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: no-color
value_type: bool
default_value: "false"
description: Produce monochrome output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-log-prefix
value_type: bool
default_value: "false"
description: Don't print prefix in logs
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: since
value_type: string
description: |
Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: tail
shorthand: "n"
value_type: string
default_value: all
description: |
Number of lines to show from the end of the logs for each container
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: timestamps
shorthand: t
value_type: bool
default_value: "false"
description: Show timestamps
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: until
value_type: string
description: |
Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
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
================================================
FILE: docs/reference/docker_compose_ls.yaml
================================================
command: docker compose ls
short: List running compose projects
long: Lists running Compose projects
usage: docker compose ls [OPTIONS]
pname: docker compose
plink: docker_compose.yaml
options:
- option: all
shorthand: a
value_type: bool
default_value: "false"
description: Show all stopped Compose projects
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: filter
value_type: filter
description: Filter output based on conditions provided
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display project names
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
================================================
FILE: docs/reference/docker_compose_pause.yaml
================================================
command: docker compose pause
short: Pause services
long: |
Pauses running containers of a service. They can be unpaused with `docker compose unpause`.
usage: docker compose pause [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
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
================================================
FILE: docs/reference/docker_compose_port.yaml
================================================
command: docker compose port
short: Print the public port for a port binding
long: Prints the public port for a port binding
usage: docker compose port [OPTIONS] SERVICE PRIVATE_PORT
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: protocol
value_type: string
default_value: tcp
description: tcp or udp
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
================================================
FILE: docs/reference/docker_compose_ps.yaml
================================================
command: docker compose ps
short: List containers
long: |-
Lists containers for a Compose project, with current status and exposed ports.
```console
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
```
By default, only running containers are shown. `--all` flag can be used to include stopped containers.
```console
$ docker compose ps --all
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
example-bar-1 alpine "/entrypoint.…" bar 4 seconds ago exited (0)
```
usage: docker compose ps [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: all
shorthand: a
value_type: bool
default_value: "false"
description: |
Show all stopped containers (including those created by the run command)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: filter
value_type: string
description: 'Filter services by a property (supported filters: status)'
details_url: '#filter'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: format
value_type: string
default_value: table
description: |-
Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
details_url: '#format'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-trunc
value_type: bool
default_value: "false"
description: Don't truncate output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: orphans
value_type: bool
default_value: "true"
description: Include orphaned services (not declared by project)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display IDs
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: services
value_type: bool
default_value: "false"
description: Display services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: status
value_type: stringArray
default_value: '[]'
description: |
Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]
details_url: '#status'
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
examples: |-
### Format the output (--format) {#format}
By default, the `docker compose ps` command uses a table ("pretty") format to
show the containers. The `--format` flag allows you to specify alternative
presentations for the output. Currently, supported options are `pretty` (default),
and `json`, which outputs information about the containers as a JSON array:
```console
$ docker compose ps --format json
[{"ID":"1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a","Name":"example-bar-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"bar","State":"exited","Health":"","ExitCode":0,"Publishers":null},{"ID":"f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0","Name":"example-foo-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"foo","State":"running","Health":"","ExitCode":0,"Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"}]}]
```
The JSON output allows you to use the information in other tools for further
processing, for example, using the [`jq` utility](https://stedolan.github.io/jq/)
to pretty-print the JSON:
```console
$ docker compose ps --format json | jq .
[
{
"ID": "1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a",
"Name": "example-bar-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "bar",
"State": "exited",
"Health": "",
"ExitCode": 0,
"Publishers": null
},
{
"ID": "f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0",
"Name": "example-foo-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "foo",
"State": "running",
"Health": "",
"ExitCode": 0,
"Publishers": [
{
"URL": "0.0.0.0",
"TargetPort": 80,
"PublishedPort": 8080,
"Protocol": "tcp"
}
]
}
]
```
### Filter containers by status (--status) {#status}
Use the `--status` flag to filter the list of containers by status. For example,
to show only containers that are running or only containers that have exited:
```console
$ docker compose ps --status=running
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
$ docker compose ps --status=exited
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-bar-1 alpine "/entrypoint.…" bar 4 seconds ago exited (0)
```
### Filter containers by status (--filter) {#filter}
The [`--status` flag](#status) is a convenient shorthand for the `--filter status=`
flag. The example below is the equivalent to the example from the previous section,
this time using the `--filter` flag:
```console
$ docker compose ps --filter status=running
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
example-foo-1 alpine "/entrypoint.…" foo 4 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp
```
The `docker compose ps` command currently only supports the `--filter status=`
option, but additional filter options may be added in the future.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_publish.yaml
================================================
command: docker compose publish
short: Publish compose application
long: Publish compose application
usage: docker compose publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose
plink: docker_compose.yaml
options:
- option: app
value_type: bool
default_value: "false"
description: Published compose application (includes referenced images)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: insecure-registry
value_type: bool
default_value: "false"
description: Use insecure registry
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: oci-version
value_type: string
description: |
OCI image/artifact specification version (automatically determined by default)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
description: Pin image tags to digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: with-env
value_type: bool
default_value: "false"
description: Include environment variables in the published OCI artifact
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"
description: Assume "yes" as answer to all prompts
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
================================================
FILE: docs/reference/docker_compose_pull.yaml
================================================
command: docker compose pull
short: Pull service images
long: |
Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on those images
usage: docker compose pull [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: ignore-buildable
value_type: bool
default_value: "false"
description: Ignore images that can be built
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: ignore-pull-failures
value_type: bool
default_value: "false"
description: Pull what it can and ignores images with pull failures
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: include-deps
value_type: bool
default_value: "false"
description: Also pull services declared as dependencies
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-parallel
value_type: bool
default_value: "true"
description: DEPRECATED disable parallel pulling
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: parallel
value_type: bool
default_value: "true"
description: DEPRECATED pull multiple images in parallel
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: policy
value_type: string
description: Apply pull policy ("missing"|"always")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Pull without printing progress information
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
examples: |-
Consider the following `compose.yaml`:
```yaml
services:
db:
image: postgres
web:
build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
```
If you run `docker compose pull ServiceName` in the same directory as the `compose.yaml` file that defines the service,
Docker pulls the associated image. For example, to call the postgres image configured as the db service in our example,
you would run `docker compose pull db`.
```console
$ docker compose pull db
[+] Running 1/15
⠸ db Pulling 12.4s
⠿ 45b42c59be33 Already exists 0.0s
⠹ 40adec129f1a Downloading 3.374MB/4.178MB 9.3s
⠹ b4c431d00c78 Download complete 9.3s
⠹ 2696974e2815 Download complete 9.3s
⠹ 564b77596399 Downloading 5.622MB/7.965MB 9.3s
⠹ 5044045cf6f2 Downloading 216.7kB/391.1kB 9.3s
⠹ d736e67e6ac3 Waiting 9.3s
⠹ 390c1c9a5ae4 Waiting 9.3s
⠹ c0e62f172284 Waiting 9.3s
⠹ ebcdc659c5bf Waiting 9.3s
⠹ 29be22cb3acc Waiting 9.3s
⠹ f63c47038e66 Waiting 9.3s
⠹ 77a0c198cde5 Waiting 9.3s
⠹ c8752d5b785c Waiting 9.3s
```
`docker compose pull` tries to pull image for services with a build section. If pull fails, it lets you know this service image must be built. You can skip this by setting `--ignore-buildable` flag.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_push.yaml
================================================
command: docker compose push
short: Push service images
long: |-
Pushes images for services to their respective registry/repository.
The following assumptions are made:
- You are pushing an image you have built locally
- You have access to the build key
Examples
```yaml
services:
service1:
build: .
image: localhost:5000/yourimage ## goes to local registry
service2:
build: .
image: your-dockerid/yourimage ## goes to your repository on Docker Hub
```
usage: docker compose push [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: ignore-push-failures
value_type: bool
default_value: "false"
description: Push what it can and ignores images with push failures
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: include-deps
value_type: bool
default_value: "false"
description: Also push images of services declared as dependencies
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Push without printing progress information
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
================================================
FILE: docs/reference/docker_compose_restart.yaml
================================================
command: docker compose restart
short: Restart service containers
long: |-
Restarts all stopped and running services, or the specified services only.
If you make changes to your `compose.yml` configuration, these changes are not reflected
after running this command. For example, changes to environment variables (which are added
after a container is built, but before the container's command is executed) are not updated
after restarting.
If you are looking to configure a service's restart policy, refer to
[restart](https://github.com/compose-spec/compose-spec/blob/main/spec.md#restart)
or [restart_policy](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#restart_policy).
usage: docker compose restart [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: no-deps
value_type: bool
default_value: "false"
description: Don't restart dependent services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: timeout
shorthand: t
value_type: int
default_value: "0"
description: Specify a shutdown timeout in seconds
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
================================================
FILE: docs/reference/docker_compose_rm.yaml
================================================
command: docker compose rm
short: Removes stopped service containers
long: |-
Removes stopped service containers.
By default, anonymous volumes attached to containers are not removed. You can override this with `-v`. To list all
volumes, use `docker volume ls`.
Any data which is not in a volume is lost.
Running the command with no options also removes one-off containers created by `docker compose run`:
```console
$ docker compose rm
Going to remove djangoquickstart_web_run_1
Are you sure? [yN] y
Removing djangoquickstart_web_run_1 ... done
```
usage: docker compose rm [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: all
shorthand: a
value_type: bool
default_value: "false"
description: Deprecated - no effect
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: force
shorthand: f
value_type: bool
default_value: "false"
description: Don't ask to confirm removal
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: stop
shorthand: s
value_type: bool
default_value: "false"
description: Stop the containers, if required, before removing
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: volumes
shorthand: v
value_type: bool
default_value: "false"
description: Remove any anonymous volumes attached to containers
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
================================================
FILE: docs/reference/docker_compose_run.yaml
================================================
command: docker compose run
short: Run a one-off command on a service
long: |-
Runs a one-time command against a service.
The following command starts the `web` service and runs `bash` as its command:
```console
$ docker compose run web bash
```
Commands you use with run start in new containers with configuration defined by that of the service,
including volumes, links, and other details. However, there are two important differences:
First, the command passed by `run` overrides the command defined in the service configuration. For example, if the
`web` service configuration is started with `bash`, then `docker compose run web python app.py` overrides it with
`python app.py`.
The second difference is that the `docker compose run` command does not create any of the ports specified in the
service configuration. This prevents port collisions with already-open ports. If you do want the service’s ports
to be created and mapped to the host, specify the `--service-ports`
```console
$ docker compose run --service-ports web python manage.py shell
```
Alternatively, manual port mapping can be specified with the `--publish` or `-p` options, just as when using docker run:
```console
$ docker compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell
```
If you start a service configured with links, the run command first checks to see if the linked service is running
and starts the service if it is stopped. Once all the linked services are running, the run executes the command you
passed it. For example, you could run:
```console
$ docker compose run db psql -h db -U docker
```
This opens an interactive PostgreSQL shell for the linked `db` container.
If you do not want the run command to start linked containers, use the `--no-deps` flag:
```console
$ docker compose run --no-deps web python manage.py shell
```
If you want to remove the container after running while overriding the container’s restart policy, use the `--rm` flag:
```console
$ docker compose run --rm web python manage.py db upgrade
```
This runs a database upgrade script, and removes the container when finished running, even if a restart policy is
specified in the service configuration.
usage: docker compose run [OPTIONS] SERVICE [COMMAND] [ARGS...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: build
value_type: bool
default_value: "false"
description: Build image before starting container
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: cap-add
value_type: list
description: Add Linux capabilities
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: cap-drop
value_type: list
description: Drop Linux capabilities
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: detach
shorthand: d
value_type: bool
default_value: "false"
description: Run container in background and print container ID
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: entrypoint
value_type: string
description: Override the entrypoint of the image
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: env
shorthand: e
value_type: stringArray
default_value: '[]'
description: Set environment variables
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: env-from-file
value_type: stringArray
default_value: '[]'
description: Set environment variables from file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: interactive
shorthand: i
value_type: bool
default_value: "true"
description: Keep STDIN open even if not attached
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: label
shorthand: l
value_type: stringArray
default_value: '[]'
description: Add or override a label
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: name
value_type: string
description: Assign a name to the container
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-TTY
shorthand: T
value_type: bool
default_value: "true"
description: 'Disable pseudo-TTY allocation (default: auto-detected)'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-deps
value_type: bool
default_value: "false"
description: Don't start linked services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: publish
shorthand: p
value_type: stringArray
default_value: '[]'
description: Publish a container's port(s) to the host
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pull
value_type: string
default_value: policy
description: Pull image before running ("always"|"missing"|"never")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Don't print anything to STDOUT
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-build
value_type: bool
default_value: "false"
description: Suppress progress output from the build process
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"
description: Pull without printing progress information
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: rm
value_type: bool
default_value: "false"
description: Automatically remove the container when it exits
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: service-ports
shorthand: P
value_type: bool
default_value: "false"
description: |
Run command with all service's ports enabled and mapped to the host
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: tty
shorthand: t
value_type: bool
default_value: "true"
description: Allocate a pseudo-TTY
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: use-aliases
value_type: bool
default_value: "false"
description: |
Use the service's network useAliases in the network(s) the container connects to
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: user
shorthand: u
value_type: string
description: Run as specified username or uid
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: volume
shorthand: v
value_type: stringArray
default_value: '[]'
description: Bind mount a volume
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: workdir
shorthand: w
value_type: string
description: Working directory inside the container
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
================================================
FILE: docs/reference/docker_compose_scale.yaml
================================================
command: docker compose scale
short: Scale services
long: Scale services
usage: docker compose scale [SERVICE=REPLICAS...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: no-deps
value_type: bool
default_value: "false"
description: Don't start linked services
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
================================================
FILE: docs/reference/docker_compose_start.yaml
================================================
command: docker compose start
short: Start services
long: Starts existing containers for a service
usage: docker compose start [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: wait
value_type: bool
default_value: "false"
description: Wait for services to be running|healthy. Implies detached mode.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait-timeout
value_type: int
default_value: "0"
description: |
Maximum duration in seconds to wait for the project to be running|healthy
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
================================================
FILE: docs/reference/docker_compose_stats.yaml
================================================
command: docker compose stats
short: Display a live stream of container(s) resource usage statistics
long: Display a live stream of container(s) resource usage statistics
usage: docker compose stats [OPTIONS] [SERVICE]
pname: docker compose
plink: docker_compose.yaml
options:
- option: all
shorthand: a
value_type: bool
default_value: "false"
description: Show all containers (default shows just running)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: format
value_type: string
description: |-
Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-stream
value_type: bool
default_value: "false"
description: Disable streaming stats and only pull the first result
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-trunc
value_type: bool
default_value: "false"
description: Do not truncate output
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
================================================
FILE: docs/reference/docker_compose_stop.yaml
================================================
command: docker compose stop
short: Stop services
long: |
Stops running containers without removing them. They can be started again with `docker compose start`.
usage: docker compose stop [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: timeout
shorthand: t
value_type: int
default_value: "0"
description: Specify a shutdown timeout in seconds
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
================================================
FILE: docs/reference/docker_compose_top.yaml
================================================
command: docker compose top
short: Display the running processes
long: Displays the running processes
usage: docker compose top [SERVICES...]
pname: docker compose
plink: docker_compose.yaml
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
examples: |-
```console
$ docker compose top
example_foo_1
UID PID PPID C STIME TTY TIME CMD
root 142353 142331 2 15:33 ? 00:00:00 ping localhost -c 5
```
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_unpause.yaml
================================================
command: docker compose unpause
short: Unpause services
long: Unpauses paused containers of a service
usage: docker compose unpause [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
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
================================================
FILE: docs/reference/docker_compose_up.yaml
================================================
command: docker compose up
short: Create and start containers
long: |-
Builds, (re)creates, starts, and attaches to containers for a service.
Unless they are already running, this command also starts any linked services.
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
One can optionally select a subset of services to attach to using `--attach` flag, or exclude some services using
`--no-attach` to prevent output to be flooded by some verbose services.
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
background and leaves them running.
If there are existing containers for a service, and the service’s configuration or image was changed after the
container’s creation, `docker compose up` picks up the changes by stopping and recreating the containers
(preserving mounted volumes). To prevent Compose from picking up changes, use the `--no-recreate` flag.
If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag.
If the process encounters an error, the exit code for this command is `1`.
If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the containers are stopped, and the exit code is `0`.
usage: docker compose up [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: abort-on-container-exit
value_type: bool
default_value: "false"
description: |
Stops all containers if any container was stopped. Incompatible with -d
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: abort-on-container-failure
value_type: bool
default_value: "false"
description: |
Stops all containers if any container exited with failure. Incompatible with -d
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: always-recreate-deps
value_type: bool
default_value: "false"
description: Recreate dependent containers. Incompatible with --no-recreate.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: attach
value_type: stringArray
default_value: '[]'
description: |
Restrict attaching to the specified services. Incompatible with --attach-dependencies.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: attach-dependencies
value_type: bool
default_value: "false"
description: Automatically attach to log output of dependent services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: build
value_type: bool
default_value: "false"
description: Build images before starting containers
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: detach
shorthand: d
value_type: bool
default_value: "false"
description: 'Detached mode: Run containers in the background'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: exit-code-from
value_type: string
description: |
Return the exit code of the selected service container. Implies --abort-on-container-exit
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: force-recreate
value_type: bool
default_value: "false"
description: |
Recreate containers even if their configuration and image haven't changed
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: menu
value_type: bool
default_value: "false"
description: |
Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-attach
value_type: stringArray
default_value: '[]'
description: Do not attach (stream logs) to the specified services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-build
value_type: bool
default_value: "false"
description: Don't build an image, even if it's policy
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-color
value_type: bool
default_value: "false"
description: Produce monochrome output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-deps
value_type: bool
default_value: "false"
description: Don't start linked services
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-log-prefix
value_type: bool
default_value: "false"
description: Don't print prefix in logs
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-recreate
value_type: bool
default_value: "false"
description: |
If containers already exist, don't recreate them. Incompatible with --force-recreate.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-start
value_type: bool
default_value: "false"
description: Don't start the services after creating them
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pull
value_type: string
default_value: policy
description: Pull image before running ("always"|"missing"|"never")
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-build
value_type: bool
default_value: "false"
description: Suppress the build output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"
description: Pull without printing progress information
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: renew-anon-volumes
shorthand: V
value_type: bool
default_value: "false"
description: |
Recreate anonymous volumes instead of retrieving data from the previous containers
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: scale
value_type: stringArray
default_value: '[]'
description: |
Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: timeout
shorthand: t
value_type: int
default_value: "0"
description: |
Use this timeout in seconds for container shutdown when attached or when containers are already running
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: timestamps
value_type: bool
default_value: "false"
description: Show timestamps
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait
value_type: bool
default_value: "false"
description: Wait for services to be running|healthy. Implies detached mode.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait-timeout
value_type: int
default_value: "0"
description: |
Maximum duration in seconds to wait for the project to be running|healthy
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: watch
shorthand: w
value_type: bool
default_value: "false"
description: |
Watch source code and rebuild/refresh containers when files are updated.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"
description: Assume "yes" as answer to all prompts and run non-interactively
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
================================================
FILE: docs/reference/docker_compose_version.yaml
================================================
command: docker compose version
short: Show the Docker Compose version information
long: Show the Docker Compose version information
usage: docker compose version [OPTIONS]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
shorthand: f
value_type: string
description: 'Format the output. Values: [pretty | json]. (Default: pretty)'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: short
value_type: bool
default_value: "false"
description: Shows only Compose's version number
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
================================================
FILE: docs/reference/docker_compose_volumes.yaml
================================================
command: docker compose volumes
short: List volumes
long: List volumes
usage: docker compose volumes [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
value_type: string
default_value: table
description: |-
Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display volume names
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
================================================
FILE: docs/reference/docker_compose_wait.yaml
================================================
command: docker compose wait
short: Block until containers of all (or specified) services stop.
long: Block until containers of all (or specified) services stop.
usage: docker compose wait SERVICE [SERVICE...] [OPTIONS]
pname: docker compose
plink: docker_compose.yaml
options:
- option: down-project
value_type: bool
default_value: "false"
description: Drops project when the first container stops
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
================================================
FILE: docs/reference/docker_compose_watch.yaml
================================================
command: docker compose watch
short: |
Watch build context for service and rebuild/refresh containers when files are updated
long: |
Watch build context for service and rebuild/refresh containers when files are updated
usage: docker compose watch [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: no-up
value_type: bool
default_value: "false"
description: Do not build & start services before watching
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: prune
value_type: bool
default_value: "true"
description: Prune dangling images on rebuild
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
value_type: bool
default_value: "false"
description: hide build output
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
================================================
FILE: docs/sdk.md
================================================
# Using the `docker/compose` SDK
The `docker/compose` package can be used as a Go library by third-party applications to programmatically manage
containerized applications defined in Compose files. This SDK provides a comprehensive API that lets you
integrate Compose functionality directly into your applications, allowing you to load, validate, and manage
multi-container environments without relying on the Compose CLI.
Whether you need to orchestrate containers as part of
a deployment pipeline, build custom management tools, or embed container orchestration into your application, the
Compose SDK offers the same powerful capabilities that drive the Docker Compose command-line tool.
## Set up the SDK
To get started, create an SDK instance using the `NewComposeService()` function, which initializes a service with the
necessary configuration to interact with the Docker daemon and manage Compose projects. This service instance provides
methods for all core Compose operations including creating, starting, stopping, and removing containers, as well as
loading and validating Compose files. The service handles the underlying Docker API interactions and resource
management, allowing you to focus on your application logic.
## Example usage
Here's a basic example demonstrating how to load a Compose project and start the services:
```go
package main
import (
"context"
"log"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
func main() {
ctx := context.Background()
dockerCLI, err := command.NewDockerCli()
if err != nil {
log.Fatalf("Failed to create docker CLI: %v", err)
}
err = dockerCLI.Initialize(&flags.ClientOptions{})
if err != nil {
log.Fatalf("Failed to initialize docker CLI: %v", err)
}
// Create a new Compose service instance
service, err := compose.NewComposeService(dockerCLI)
if err != nil {
log.Fatalf("Failed to create compose service: %v", err)
}
// Load the Compose project from a compose file
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
ConfigPaths: []string{"compose.yaml"},
ProjectName: "my-app",
})
if err != nil {
log.Fatalf("Failed to load project: %v", err)
}
// Start the services defined in the Compose file
err = service.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{},
Start: api.StartOptions{},
})
if err != nil {
log.Fatalf("Failed to start services: %v", err)
}
log.Printf("Successfully started project: %s", project.Name)
}
```
This example demonstrates the core workflow - creating a service instance, loading a project from a Compose file, and
starting the services. The SDK provides many additional operations for managing the lifecycle of your containerized
application.
## Customizing the SDK
The `NewComposeService()` function accepts optional `compose.Option` parameters to customize the SDK behavior. These
options allow you to configure I/O streams, concurrency limits, dry-run mode, and other advanced features.
```go
// Create a custom output buffer to capture logs
var outputBuffer bytes.Buffer
// Create a compose service with custom options
service, err := compose.NewComposeService(dockerCLI,
compose.WithOutputStream(&outputBuffer), // Redirect output to custom writer
compose.WithErrorStream(os.Stderr), // Use stderr for errors
compose.WithMaxConcurrency(4), // Limit concurrent operations
compose.WithPrompt(compose.AlwaysOkPrompt()), // Auto-confirm all prompts
)
```
### Available options
- `WithOutputStream(io.Writer)` - Redirect standard output to a custom writer
- `WithErrorStream(io.Writer)` - Redirect error output to a custom writer
- `WithInputStream(io.Reader)` - Provide a custom input stream for interactive prompts
- `WithStreams(out, err, in)` - Set all I/O streams at once
- `WithMaxConcurrency(int)` - Limit the number of concurrent operations against the Docker API
- `WithPrompt(Prompt)` - Customize user confirmation behavior (use `AlwaysOkPrompt()` for non-interactive mode)
- `WithDryRun` - Run operations in dry-run mode without actually applying changes
- `WithContextInfo(api.ContextInfo)` - Set custom Docker context information
- `WithProxyConfig(map[string]string)` - Configure HTTP proxy settings for builds
- `WithEventProcessor(progress.EventProcessor)` - Receive progress events and operation notifications
These options provide fine-grained control over the SDK's behavior, making it suitable for various integration
scenarios including CLI tools, web services, automation scripts, and testing environments.
## Tracking operations with `EventProcessor`
The `EventProcessor` interface allows you to monitor Compose operations in real-time by receiving events about changes
applied to Docker resources such as images, containers, volumes, and networks. This is particularly useful for building
user interfaces, logging systems, or monitoring tools that need to track the progress of Compose operations.
### Understanding `EventProcessor`
A Compose operation, such as `up`, `down`, `build`, performs a series of changes to Docker resources. The
`EventProcessor` receives notifications about these changes through three key methods:
- `Start(ctx, operation)` - Called when a Compose operation begins, for example `up`
- `On(events...)` - Called with progress events for individual resource changes, for example, container starting, image
being pulled
- `Done(operation, success)` - Called when the operation completes, indicating success or failure
Each event contains information about the resource being modified, its current status, and progress indicators when
applicable (such as download progress for image pulls).
### Event status types
Events report resource changes with the following status types:
- Working - Operation is in progress, for example, creating, starting, pulling
- Done - Operation completed successfully
- Warning - Operation completed with warnings
- Error - Operation failed
Common status text values include: `Creating`, `Created`, `Starting`, `Started`, `Running`, `Stopping`, `Stopped`,
`Removing`, `Removed`, `Building`, `Built`, `Pulling`, `Pulled`, and more.
### Built-in `EventProcessor` implementations
The SDK provides three ready-to-use `EventProcessor` implementations:
- `progress.NewTTYWriter(io.Writer)` - Renders an interactive terminal UI with progress bars and task lists
(similar to the Docker Compose CLI output)
- `progress.NewPlainWriter(io.Writer)` - Outputs simple text-based progress messages suitable for non-interactive
environments or log files
- `progress.NewJSONWriter()` - Render events as JSON objects
- `progress.NewQuietWriter()` - (Default) Silently processes events without producing any output
Using `EventProcessor`, a custom UI can be plugged into `docker/compose`.
================================================
FILE: docs/yaml/main/generate.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"path/filepath"
clidocstool "github.com/docker/cli-docs-tool"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/compose"
)
func generateDocs(opts *options) error {
dockerCLI, err := command.NewDockerCli()
if err != nil {
return err
}
cmd := &cobra.Command{
Use: "docker",
DisableAutoGenTag: true,
}
cmd.AddCommand(compose.RootCommand(dockerCLI, nil))
disableFlagsInUseLine(cmd)
tool, err := clidocstool.New(clidocstool.Options{
Root: cmd,
SourceDir: opts.source,
TargetDir: opts.target,
Plugin: true,
})
if err != nil {
return err
}
for _, format := range opts.formats {
switch format {
case "yaml":
if err := tool.GenYamlTree(cmd); err != nil {
return err
}
case "md":
if err := tool.GenMarkdownTree(cmd); err != nil {
return err
}
default:
return fmt.Errorf("unknown format %q", format)
}
}
return nil
}
func disableFlagsInUseLine(cmd *cobra.Command) {
visitAll(cmd, func(ccmd *cobra.Command) {
// do not add a `[flags]` to the end of the usage line.
ccmd.DisableFlagsInUseLine = true
})
}
// visitAll will traverse all commands from the root.
// This is different from the VisitAll of cobra.Command where only parents
// are checked.
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
for _, cmd := range root.Commands() {
visitAll(cmd, fn)
}
fn(root)
}
type options struct {
source string
target string
formats []string
}
func main() {
cwd, _ := os.Getwd()
opts := &options{
source: filepath.Join(cwd, "docs", "reference"),
target: filepath.Join(cwd, "docs", "reference"),
formats: []string{"yaml", "md"},
}
fmt.Printf("Project root: %s\n", opts.source)
fmt.Printf("Generating yaml files into %s\n", opts.target)
if err := generateDocs(opts); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Failed to generate documentation: %s\n", err.Error())
}
}
================================================
FILE: go.mod
================================================
module github.com/docker/compose/v5
go 1.25.0
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e
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.10.1
github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.2.2
github.com/containerd/errdefs v1.0.0
github.com/containerd/platforms v1.0.0-rc.2
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.31.1
github.com/docker/cli v29.2.1+incompatible
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/fsnotify/fsevents v0.2.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-version v1.8.0
github.com/jonboulle/clockwork v0.5.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0
github.com/moby/buildkit v0.27.1
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.54.0
github.com/moby/moby/client v0.3.0
github.com/moby/patternmatcher v0.6.0
github.com/moby/sys/atomicwriter v0.1.0
github.com/morikuni/aec v1.1.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/otiai10/copy v1.14.1
github.com/sirupsen/logrus v1.9.4
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.6.0
go.yaml.in/yaml/v4 v4.0.0-rc.4
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
google.golang.org/grpc v1.78.0
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.1.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/containerd/containerd/api v1.10.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/symlink v0.3.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/sigstore v1.10.0 // indirect
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
exclude (
// FIXME(thaJeztah): remove this once kubernetes updated their dependencies to no longer need this.
//
// For additional details, see this PR and links mentioned in that PR:
// https://github.com/kubernetes-sigs/kustomize/pull/5830#issuecomment-2569960859
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
)
================================================
FILE: go.sum
================================================
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=
github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=
github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/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.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU=
github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg=
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
github.com/containerd/containerd/v2 v2.2.2 h1:mjVQdtfryzT7lOqs5EYUFZm8ioPVjOpkSoG1GJPxEMY=
github.com/containerd/containerd/v2 v2.2.2/go.mod h1:5Jhevmv6/2J+Iu/A2xXAdUIdI5Ah/hfyO7okJ4AFIdY=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
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=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.10 h1:hphjuKOqSHLGznNJiAvmsOWkdu4qFXjf4DzGrWSuIsM=
github.com/containerd/nydus-snapshotter v0.15.10/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/buildx v0.31.1 h1:zbvbrb9nxBNVV8nnI33f2F+4aAZBA1gY+AmeBFflMqY=
github.com/docker/buildx v0.31.1/go.mod h1:SD+jYLnt3S4SXqohVtV+8z+dihnOgwMJ8t+bLQvsaCk=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
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 v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0=
github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.25.3 h1:FAa5wJXyDtI7yUztKDfZxDrSx+8WTg31MfCQ9s3PV+s=
github.com/go-openapi/swag v0.25.3/go.mod h1:tX9vI8Mj8Ny+uCEk39I1QADvIPI7lkndX4qCsEqhkS8=
github.com/go-openapi/swag/cmdutils v0.25.3 h1:EIwGxN143JCThNHnqfqs85R8lJcJG06qjJRZp3VvjLI=
github.com/go-openapi/swag/cmdutils v0.25.3/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E=
github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU=
github.com/go-openapi/swag/fileutils v0.25.3 h1:P52Uhd7GShkeU/a1cBOuqIcHMHBrA54Z2t5fLlE85SQ=
github.com/go-openapi/swag/fileutils v0.25.3/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw=
github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o=
github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y=
github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c=
github.com/go-openapi/swag/mangling v0.25.3 h1:rGIrEzXaYWuUW1MkFmG3pcH+EIA0/CoUkQnIyB6TUyo=
github.com/go-openapi/swag/mangling v0.25.3/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.3 h1:XWXHZfL/65ABiv8rvGp9dtE0C6QHTYkCrNV77jTl358=
github.com/go-openapi/swag/netutils v0.25.3/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w=
github.com/go-openapi/swag/stringutils v0.25.3/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri2fdDV4t2TQc=
github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg=
github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/moby/buildkit v0.27.1 h1:qlIWpnZzqCkrYiGkctM1gBD/YZPOJTjtUdRBlI0oBOU=
github.com/moby/buildkit v0.27.1/go.mod h1:99qLrCrIAFgEOiFnCi9Y0Wwp6/qA7QvZ3uq/6wF0IsI=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY=
github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/rekor v1.4.3 h1:2+aw4Gbgumv8vYM/QVg6b+hvr4x4Cukur8stJrVPKU0=
github.com/sigstore/rekor v1.4.3/go.mod h1:o0zgY087Q21YwohVvGwV9vK1/tliat5mfnPiVI3i75o=
github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo=
github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU=
github.com/sigstore/sigstore v1.10.0 h1:lQrmdzqlR8p9SCfWIpFoGUqdXEzJSZT2X+lTXOMPaQI=
github.com/sigstore/sigstore v1.10.0/go.mod h1:Ygq+L/y9Bm3YnjpJTlQrOk/gXyrjkpn3/AEJpmk1n9Y=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 h1:94NLPmq4bxvdmslzcG670IOkrlS98CGpmob8cjpFHuI=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7/go.mod h1:4r/PNX0G7uzkLpc3PSdYs5E2k4bWEJNXTK6kwAyw9TM=
github.com/sigstore/timestamp-authority/v2 v2.0.2 h1:WavlEeLh6HKt+osbmsHDg6/FaM/8Pz9iVUMh9pAsl/o=
github.com/sigstore/timestamp-authority/v2 v2.0.2/go.mod h1:D+wbQg8ASQzKnwBhLo7rIJD+9Zev4Ppqd4myPe8k57E=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
github.com/theupdateframework/go-tuf/v2 v2.3.0 h1:gt3X8xT8qu/HT4w+n1jgv+p7koi5ad8XEkLXXZqG9AA=
github.com/theupdateframework/go-tuf/v2 v2.3.0/go.mod h1:xW8yNvgXRncmovMLvBxKwrKpsOwJZu/8x+aB0KtFcdw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw=
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY=
tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q=
================================================
FILE: internal/desktop/client.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package desktop
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/internal/memnet"
)
// identify this client in the logs
var userAgent = "compose/" + internal.Version
// Client for integration with Docker Desktop features.
type Client struct {
apiEndpoint string
client *http.Client
}
// NewClient creates a Desktop integration client for the provided in-memory
// socket address (AF_UNIX or named pipe).
func NewClient(apiEndpoint string) *Client {
var transport http.RoundTripper = &http.Transport{
DisableCompression: true,
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return memnet.DialEndpoint(ctx, apiEndpoint)
},
}
transport = otelhttp.NewTransport(transport)
return &Client{
apiEndpoint: apiEndpoint,
client: &http.Client{Transport: transport},
}
}
func (c *Client) Endpoint() string {
return c.apiEndpoint
}
// Close releases any open connections.
func (c *Client) Close() error {
c.client.CloseIdleConnections()
return nil
}
type PingResponse struct {
ServerTime int64 `json:"serverTime"`
}
// Ping is a minimal API used to ensure that the server is available.
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/ping", http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var ret PingResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return &ret, nil
}
type FeatureFlagResponse map[string]FeatureFlagValue
type FeatureFlagValue struct {
Enabled bool `json:"enabled"`
}
func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlagResponse, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/features", http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var ret FeatureFlagResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return ret, nil
}
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, backendURL(path), body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
return req, nil
}
// backendURL generates a URL for the given API path.
//
// NOTE: Custom transport handles communication. The host is to create a valid
// URL for the Go http.Client that is also descriptive in error/logs.
func backendURL(path string) string {
return "http://docker-desktop/" + strings.TrimPrefix(path, "/")
}
================================================
FILE: internal/desktop/client_test.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package desktop
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestClientPing(t *testing.T) {
if testing.Short() {
t.Skip("Skipped in short mode - test connects to Docker Desktop")
}
desktopEndpoint := os.Getenv("COMPOSE_TEST_DESKTOP_ENDPOINT")
if desktopEndpoint == "" {
t.Skip("Skipping - COMPOSE_TEST_DESKTOP_ENDPOINT not defined")
}
client := NewClient(desktopEndpoint)
t.Cleanup(func() {
_ = client.Close()
})
now := time.Now()
ret, err := client.Ping(t.Context())
require.NoError(t, err)
serverTime := time.Unix(0, ret.ServerTime)
require.True(t, now.Before(serverTime))
}
================================================
FILE: internal/experimental/experimental.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package experimental
import (
"context"
"os"
"strconv"
"github.com/docker/compose/v5/internal/desktop"
)
// envComposeExperimentalGlobal can be set to a falsy value (e.g. 0, false) to
// globally opt-out of any experimental features in Compose.
const envComposeExperimentalGlobal = "COMPOSE_EXPERIMENTAL"
// State of experiments (enabled/disabled) based on environment and local config.
type State struct {
// active is false if experiments have been opted-out of globally.
active bool
desktopValues desktop.FeatureFlagResponse
}
func NewState() *State {
// experimental features have individual controls, but users can opt out
// of ALL experiments easily if desired
experimentsActive := true
if v := os.Getenv(envComposeExperimentalGlobal); v != "" {
experimentsActive, _ = strconv.ParseBool(v)
}
return &State{
active: experimentsActive,
}
}
func (s *State) Load(ctx context.Context, client *desktop.Client) error {
if !s.active {
// user opted out of experiments globally, no need to load state from
// Desktop
return nil
}
if client == nil {
// not running under Docker Desktop
return nil
}
desktopValues, err := client.FeatureFlags(ctx)
if err != nil {
return err
}
s.desktopValues = desktopValues
return nil
}
================================================
FILE: internal/locker/pidfile.go
================================================
/*
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 locker
import (
"fmt"
"path/filepath"
)
type Pidfile struct {
path string
}
func NewPidfile(projectName string) (*Pidfile, error) {
run, err := runDir()
if err != nil {
return nil, err
}
path := filepath.Join(run, fmt.Sprintf("%s.pid", projectName))
return &Pidfile{path: path}, nil
}
================================================
FILE: internal/locker/pidfile_unix.go
================================================
//go:build !windows
/*
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 locker
import (
"os"
"github.com/docker/docker/pkg/pidfile"
)
func (f *Pidfile) Lock() error {
return pidfile.Write(f.path, os.Getpid())
}
================================================
FILE: internal/locker/pidfile_windows.go
================================================
//go:build windows
/*
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 locker
import (
"os"
"github.com/docker/docker/pkg/pidfile"
"github.com/mitchellh/go-ps"
)
func (f *Pidfile) Lock() error {
newPID := os.Getpid()
err := pidfile.Write(f.path, newPID)
if err != nil {
// Get PID registered in the file
pid, errPid := pidfile.Read(f.path)
if errPid != nil {
return err
}
// Some users faced issues on Windows where the process written in the pidfile was identified as still existing
// So we used a 2nd process library to verify if this not a false positive feedback
// Check if the process exists
process, errPid := ps.FindProcess(pid)
if process == nil && errPid == nil {
// If the process does not exist, remove the pidfile and try to lock again
_ = os.Remove(f.path)
return pidfile.Write(f.path, newPID)
}
}
return err
}
================================================
FILE: internal/locker/runtime.go
================================================
/*
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 locker
import (
"os"
)
func runDir() (string, error) {
run, ok := os.LookupEnv("XDG_RUNTIME_DIR")
if ok {
return run, nil
}
path, err := osDependentRunDir()
if err != nil {
return "", err
}
err = os.MkdirAll(path, 0o700)
return path, err
}
================================================
FILE: internal/locker/runtime_darwin.go
================================================
/*
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 locker
import (
"os"
"path/filepath"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentRunDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "Library", "Application Support", "com.docker.compose"), nil
}
================================================
FILE: internal/locker/runtime_unix.go
================================================
//go:build linux || openbsd || freebsd
/*
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 locker
import (
"os"
"path/filepath"
"strconv"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentRunDir() (string, error) {
run := filepath.Join("run", "user", strconv.Itoa(os.Getuid()))
if _, err := os.Stat(run); err == nil {
return run, nil
}
// /run/user/$uid is set by pam_systemd, but might not be present, especially in containerized environments
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".docker", "docker-compose"), nil
}
================================================
FILE: internal/locker/runtime_windows.go
================================================
/*
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 locker
import (
"os"
"path/filepath"
"golang.org/x/sys/windows"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentRunDir() (string, error) {
flags := []uint32{windows.KF_FLAG_DEFAULT, windows.KF_FLAG_DEFAULT_PATH}
for _, flag := range flags {
p, _ := windows.KnownFolderPath(windows.FOLDERID_LocalAppData, flag|windows.KF_FLAG_DONT_VERIFY)
if p != "" {
return filepath.Join(p, "docker-compose"), nil
}
}
appData, ok := os.LookupEnv("LOCALAPPDATA")
if ok {
return filepath.Join(appData, "docker-compose"), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "AppData", "Local", "docker-compose"), nil
}
================================================
FILE: internal/memnet/conn.go
================================================
/*
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 memnet
import (
"context"
"fmt"
"net"
"strings"
)
func DialEndpoint(ctx context.Context, endpoint string) (net.Conn, error) {
if addr, ok := strings.CutPrefix(endpoint, "unix://"); ok {
return Dial(ctx, "unix", addr)
}
if addr, ok := strings.CutPrefix(endpoint, "npipe://"); ok {
return Dial(ctx, "npipe", addr)
}
return nil, fmt.Errorf("unsupported protocol for address: %s", endpoint)
}
func Dial(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
switch network {
case "unix":
if err := validateSocketPath(addr); err != nil {
return nil, err
}
return d.DialContext(ctx, "unix", addr)
case "npipe":
// N.B. this will return an error on non-Windows
return dialNamedPipe(ctx, addr)
default:
return nil, fmt.Errorf("unsupported network: %s", network)
}
}
================================================
FILE: internal/memnet/conn_unix.go
================================================
//go:build !windows
/*
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 memnet
import (
"context"
"fmt"
"net"
"syscall"
)
const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)
func dialNamedPipe(_ context.Context, _ string) (net.Conn, error) {
return nil, fmt.Errorf("named pipes are only available on Windows")
}
func validateSocketPath(addr string) error {
if len(addr) > maxUnixSocketPathSize {
return fmt.Errorf("socket address is too long: %s", addr)
}
return nil
}
================================================
FILE: internal/memnet/conn_windows.go
================================================
/*
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 memnet
import (
"context"
"net"
"github.com/Microsoft/go-winio"
)
func dialNamedPipe(ctx context.Context, addr string) (net.Conn, error) {
return winio.DialPipeContext(ctx, addr)
}
func validateSocketPath(addr string) error {
// AF_UNIX sockets do not have strict path limits on Windows
return nil
}
================================================
FILE: internal/oci/push.go
================================================
/*
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 oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"slices"
"time"
"github.com/containerd/containerd/v2/core/remotes"
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/compose/v5/pkg/api"
)
const (
// ComposeProjectArtifactType is the OCI 1.1-compliant artifact type value
// for the generated image manifest.
ComposeProjectArtifactType = "application/vnd.docker.compose.project"
// ComposeYAMLMediaType is the media type for each layer (Compose file)
// in the image manifest.
ComposeYAMLMediaType = "application/vnd.docker.compose.file+yaml"
// ComposeEmptyConfigMediaType is a media type used for the config descriptor
// when doing OCI 1.0-style pushes.
//
// The content is always `{}`, the same as a normal empty descriptor, but
// the specific media type allows clients to fall back to the config media
// type to recognize the manifest as a Compose project since the artifact
// type field is not available in OCI 1.0.
//
// This is based on guidance from the OCI 1.1 spec:
// > Implementers note: artifacts have historically been created without
// > an artifactType field, and tooling to work with artifacts should
// > fallback to the config.mediaType value.
ComposeEmptyConfigMediaType = "application/vnd.docker.compose.config.empty.v1+json"
// ComposeEnvFileMediaType is the media type for each Env File layer in the image manifest.
ComposeEnvFileMediaType = "application/vnd.docker.compose.envfile"
)
// clientAuthStatusCodes are client (4xx) errors that are authentication
// related.
var clientAuthStatusCodes = []int{
http.StatusUnauthorized,
http.StatusForbidden,
http.StatusProxyAuthRequired,
}
func DescriptorForComposeFile(path string, content []byte) v1.Descriptor {
return v1.Descriptor{
MediaType: ComposeYAMLMediaType,
Digest: digest.FromString(string(content)),
Size: int64(len(content)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.file": filepath.Base(path),
},
Data: content,
}
}
func DescriptorForEnvFile(path string, content []byte) v1.Descriptor {
return v1.Descriptor{
MediaType: ComposeEnvFileMediaType,
Digest: digest.FromString(string(content)),
Size: int64(len(content)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.envfile": filepath.Base(path),
},
Data: content,
}
}
func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, error) {
// Check if we need an extra empty layer for the manifest config
if ociVersion == api.OCIVersion1_1 || ociVersion == "" {
err := push(ctx, resolver, named, v1.DescriptorEmptyJSON)
if err != nil {
return v1.Descriptor{}, err
}
}
// prepare to push the manifest by pushing the layers
layerDescriptors := make([]v1.Descriptor, len(layers))
for i := range layers {
layerDescriptors[i] = layers[i]
if err := push(ctx, resolver, named, layers[i]); err != nil {
return v1.Descriptor{}, err
}
}
if ociVersion != "" {
// if a version was explicitly specified, use it
return createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion)
}
// try to push in the OCI 1.1 format but fallback to OCI 1.0 on 4xx errors
// (other than auth) since it's most likely the result of the registry not
// having support
descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1)
var pushErr pusherrors.ErrUnexpectedStatus
if errors.As(err, &pushErr) && isNonAuthClientError(pushErr.StatusCode) {
// TODO(milas): show a warning here (won't work with logrus)
return createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0)
}
return descriptor, err
}
func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor v1.Descriptor) error {
fullRef, err := reference.WithDigest(reference.TagNameOnly(ref), descriptor.Digest)
if err != nil {
return err
}
return Push(ctx, resolver, fullRef, descriptor)
}
func createAndPushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, error) {
descriptor, toPush, err := generateManifest(layers, ociVersion)
if err != nil {
return v1.Descriptor{}, err
}
for _, p := range toPush {
err = push(ctx, resolver, named, p)
if err != nil {
return v1.Descriptor{}, err
}
}
return descriptor, nil
}
func isNonAuthClientError(statusCode int) bool {
if statusCode < 400 || statusCode >= 500 {
// not a client error
return false
}
return !slices.Contains(clientAuthStatusCodes, statusCode)
}
func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) (v1.Descriptor, []v1.Descriptor, error) {
var toPush []v1.Descriptor
var config v1.Descriptor
var artifactType string
switch ociCompat {
case api.OCIVersion1_0:
// "Content other than OCI container images MAY be packaged using the image manifest.
// When this is done, the config.mediaType value MUST be set to a value specific to
// the artifact type or the empty value."
// Source: https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage
//
// The `ComposeEmptyConfigMediaType` is used specifically for this purpose:
// there is no config, and an empty descriptor is used for OCI 1.1 in
// conjunction with the `ArtifactType`, but for OCI 1.0 compatibility,
// tooling falls back to the config media type, so this is used to
// indicate that it's not a container image but custom content.
configData := []byte("{}")
config = v1.Descriptor{
MediaType: ComposeEmptyConfigMediaType,
Digest: digest.FromBytes(configData),
Size: int64(len(configData)),
Data: configData,
}
// N.B. OCI 1.0 does NOT support specifying the artifact type, so it's
// left as an empty string to omit it from the marshaled JSON
artifactType = ""
toPush = append(toPush, config)
case api.OCIVersion1_1:
config = v1.DescriptorEmptyJSON
artifactType = ComposeProjectArtifactType
toPush = append(toPush, config)
default:
return v1.Descriptor{}, nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
}
manifest, err := json.Marshal(v1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: v1.MediaTypeImageManifest,
ArtifactType: artifactType,
Config: config,
Layers: layers,
Annotations: map[string]string{
"org.opencontainers.image.created": time.Now().Format(time.RFC3339),
},
})
if err != nil {
return v1.Descriptor{}, nil, err
}
manifestDescriptor := v1.Descriptor{
MediaType: v1.MediaTypeImageManifest,
Digest: digest.FromString(string(manifest)),
Size: int64(len(manifest)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
ArtifactType: artifactType,
Data: manifest,
}
toPush = append(toPush, manifestDescriptor)
return manifestDescriptor, toPush, nil
}
================================================
FILE: internal/oci/resolver.go
================================================
/*
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 oci
import (
"context"
"io"
"net/url"
"slices"
"strings"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/containerd/containerd/v2/pkg/labels"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config/configfile"
"github.com/moby/buildkit/util/contentutil"
spec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/compose/v5/internal/registry"
)
// NewResolver setup an OCI Resolver based on docker/cli config to provide registry credentials
func NewResolver(config *configfile.ConfigFile, insecureRegistries ...string) remotes.Resolver {
return docker.NewResolver(docker.ResolverOptions{
Hosts: docker.ConfigureDefaultRegistries(
docker.WithAuthorizer(docker.NewDockerAuthorizer(
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
)),
docker.WithPlainHTTP(func(domain string) (bool, error) {
// Should be used for testing **only**
return slices.Contains(insecureRegistries, domain), nil
}),
),
})
}
// Get retrieves a Named OCI resource and returns OCI Descriptor and Manifest
func Get(ctx context.Context, resolver remotes.Resolver, ref reference.Named) (spec.Descriptor, []byte, error) {
_, descriptor, err := resolver.Resolve(ctx, ref.String())
if err != nil {
return spec.Descriptor{}, nil, err
}
fetcher, err := resolver.Fetcher(ctx, ref.String())
if err != nil {
return spec.Descriptor{}, nil, err
}
fetch, err := fetcher.Fetch(ctx, descriptor)
if err != nil {
return spec.Descriptor{}, nil, err
}
content, err := io.ReadAll(fetch)
if err != nil {
return spec.Descriptor{}, nil, err
}
return descriptor, content, nil
}
func Copy(ctx context.Context, resolver remotes.Resolver, image reference.Named, named reference.Named) (spec.Descriptor, error) {
src, desc, err := resolver.Resolve(ctx, image.String())
if err != nil {
return spec.Descriptor{}, err
}
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
// set LabelDistributionSource so push will actually use a registry mount
refspec := reference.TrimNamed(image).String()
u, err := url.Parse("dummy://" + refspec)
if err != nil {
return spec.Descriptor{}, err
}
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
desc.Annotations[labels.LabelDistributionSource+"."+source] = repo
p, err := resolver.Pusher(ctx, named.Name())
if err != nil {
return spec.Descriptor{}, err
}
f, err := resolver.Fetcher(ctx, src)
if err != nil {
return spec.Descriptor{}, err
}
err = contentutil.CopyChain(ctx,
contentutil.FromPusher(p),
contentutil.FromFetcher(f), desc)
return desc, err
}
func Push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor spec.Descriptor) error {
pusher, err := resolver.Pusher(ctx, ref.String())
if err != nil {
return err
}
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeYAMLMediaType, "artifact-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeEnvFileMediaType, "artifact-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeEmptyConfigMediaType, "config-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, spec.MediaTypeEmptyJSON, "config-")
push, err := pusher.Push(ctx, descriptor)
if errdefs.IsAlreadyExists(err) {
return nil
}
if err != nil {
return err
}
_, err = push.Write(descriptor.Data)
if err != nil {
// Close the writer on error since Commit won't be called
_ = push.Close()
return err
}
// Commit will close the writer
return push.Commit(ctx, int64(len(descriptor.Data)), descriptor.Digest)
}
================================================
FILE: internal/paths/paths.go
================================================
/*
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 paths
import (
"os"
"path/filepath"
"strings"
)
func IsChild(dir string, file string) bool {
if dir == "" {
return false
}
dir = filepath.Clean(dir)
current := filepath.Clean(file)
child := "."
for {
if strings.EqualFold(dir, current) {
// If the two paths are exactly equal, then they must be the same.
if dir == current {
return true
}
// If the two paths are equal under case-folding, but not exactly equal,
// then the only way to check if they're truly "equal" is to check
// to see if we're on a case-insensitive file system.
//
// This is a notoriously tricky problem. See how dep solves it here:
// https://github.com/golang/dep/blob/v0.5.4/internal/fs/fs.go#L33
//
// because you can mount case-sensitive filesystems onto case-insensitive
// file-systems, and vice versa :scream:
//
// We want to do as much of this check as possible with strings-only
// (to avoid a file system read and error handling), so we only
// do this check if we have no other choice.
dirInfo, err := os.Stat(dir)
if err != nil {
return false
}
currentInfo, err := os.Stat(current)
if err != nil {
return false
}
if !os.SameFile(dirInfo, currentInfo) {
return false
}
return true
}
if len(current) <= len(dir) || current == "." {
return false
}
cDir := filepath.Dir(current)
cBase := filepath.Base(current)
child = filepath.Join(cBase, child)
current = cDir
}
}
// EncompassingPaths returns the minimal set of paths that root all paths
// from the original collection.
//
// For example, ["/foo", "/foo/bar", "/foo", "/baz"] -> ["/foo", "/baz].
func EncompassingPaths(paths []string) []string {
result := []string{}
for _, current := range paths {
isCovered := false
hasRemovals := false
for i, existing := range result {
if IsChild(existing, current) {
// The path is already covered, so there's no need to include it
isCovered = true
break
}
if IsChild(current, existing) {
// Mark the element empty for removal.
result[i] = ""
hasRemovals = true
}
}
if !isCovered {
result = append(result, current)
}
if hasRemovals {
// Remove all the empties
newResult := []string{}
for _, r := range result {
if r != "" {
newResult = append(newResult, r)
}
}
result = newResult
}
}
return result
}
================================================
FILE: internal/registry/registry.go
================================================
/*
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 registry
const (
// DefaultNamespace is the default namespace
DefaultNamespace = "docker.io"
// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
// used for pushing and pulling images. This hostname is hard-coded to handle
// the conversion from image references without registry name (e.g. "ubuntu",
// or "ubuntu:latest"), as well as references using the "docker.io" domain
// name, which is used as canonical reference for images on Docker Hub, but
// does not match the domain-name of Docker Hub's registry.
DefaultRegistryHost = "registry-1.docker.io"
// IndexHostname is the index hostname, used for authentication and image search.
IndexHostname = "index.docker.io"
// IndexServer is used for user auth and image search
IndexServer = "https://" + IndexHostname + "/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
)
// GetAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
func GetAuthConfigKey(indexName string) string {
if indexName == IndexName || indexName == IndexHostname || indexName == DefaultRegistryHost {
return IndexServer
}
return indexName
}
================================================
FILE: internal/sync/shared.go
================================================
/*
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 sync
import (
"context"
)
// PathMapping contains the Compose service and modified host system path.
type PathMapping struct {
// HostPath that was created/modified/deleted outside the container.
//
// This is the path as seen from the user's perspective, e.g.
// - C:\Users\moby\Documents\hello-world\main.go (file on Windows)
// - /Users/moby/Documents/hello-world (directory on macOS)
HostPath string
// ContainerPath for the target file inside the container (only populated
// for sync events, not rebuild).
//
// This is the path as used in Docker CLI commands, e.g.
// - /workdir/main.go
// - /workdir/subdir
ContainerPath string
}
type Syncer interface {
Sync(ctx context.Context, service string, paths []*PathMapping) error
}
================================================
FILE: internal/sync/tar.go
================================================
/*
Copyright 2018 The Tilt Dev Authors
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 sync
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/moby/go-archive"
"github.com/moby/moby/api/types/container"
"golang.org/x/sync/errgroup"
)
type archiveEntry struct {
path string
info os.FileInfo
header *tar.Header
}
type LowLevelClient interface {
ContainersForService(ctx context.Context, projectName string, serviceName string) ([]container.Summary, error)
Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error
Untar(ctx context.Context, id string, reader io.ReadCloser) error
}
type Tar struct {
client LowLevelClient
projectName string
}
var _ Syncer = &Tar{}
func NewTar(projectName string, client LowLevelClient) *Tar {
return &Tar{
projectName: projectName,
client: client,
}
}
func (t *Tar) Sync(ctx context.Context, service string, paths []*PathMapping) error {
containers, err := t.client.ContainersForService(ctx, t.projectName, service)
if err != nil {
return err
}
var pathsToCopy []PathMapping
var pathsToDelete []string
for _, p := range paths {
if _, err := os.Stat(p.HostPath); err != nil && errors.Is(err, fs.ErrNotExist) {
pathsToDelete = append(pathsToDelete, p.ContainerPath)
} else {
pathsToCopy = append(pathsToCopy, *p)
}
}
var deleteCmd []string
if len(pathsToDelete) != 0 {
deleteCmd = append([]string{"rm", "-rf"}, pathsToDelete...)
}
var (
eg errgroup.Group
errMu sync.Mutex
errs = make([]error, 0, len(containers)*2) // max 2 errs per container
)
eg.SetLimit(16) // arbitrary limit, adjust to taste :D
for i := range containers {
containerID := containers[i].ID
tarReader := tarArchive(pathsToCopy)
eg.Go(func() error {
if len(deleteCmd) != 0 {
if err := t.client.Exec(ctx, containerID, deleteCmd, nil); err != nil {
errMu.Lock()
errs = append(errs, fmt.Errorf("deleting paths in %s: %w", containerID, err))
errMu.Unlock()
}
}
if err := t.client.Untar(ctx, containerID, tarReader); err != nil {
errMu.Lock()
errs = append(errs, fmt.Errorf("copying files to %s: %w", containerID, err))
errMu.Unlock()
}
return nil // don't fail-fast; collect all errors
})
}
_ = eg.Wait()
return errors.Join(errs...)
}
type ArchiveBuilder struct {
tw *tar.Writer
// A shared I/O buffer to help with file copying.
copyBuf *bytes.Buffer
}
func NewArchiveBuilder(writer io.Writer) *ArchiveBuilder {
tw := tar.NewWriter(writer)
return &ArchiveBuilder{
tw: tw,
copyBuf: &bytes.Buffer{},
}
}
func (a *ArchiveBuilder) Close() error {
return a.tw.Close()
}
// ArchivePathsIfExist creates a tar archive of all local files in `paths`. It quietly skips any paths that don't exist.
func (a *ArchiveBuilder) ArchivePathsIfExist(paths []PathMapping) error {
// In order to handle overlapping syncs, we
// 1) collect all the entries,
// 2) de-dupe them, with last-one-wins semantics
// 3) write all the entries
//
// It's not obvious that this is the correct behavior. A better approach
// (that's more in-line with how syncs work) might ignore files in earlier
// path mappings when we know they're going to be "synced" over.
// There's a bunch of subtle product decisions about how overlapping path
// mappings work that we're not sure about.
var entries []archiveEntry
for _, p := range paths {
newEntries, err := a.entriesForPath(p.HostPath, p.ContainerPath)
if err != nil {
return fmt.Errorf("inspecting %q: %w", p.HostPath, err)
}
entries = append(entries, newEntries...)
}
entries = dedupeEntries(entries)
for _, entry := range entries {
err := a.writeEntry(entry)
if err != nil {
return fmt.Errorf("archiving %q: %w", entry.path, err)
}
}
return nil
}
func (a *ArchiveBuilder) writeEntry(entry archiveEntry) error {
pathInTar := entry.path
header := entry.header
if header.Typeflag != tar.TypeReg {
// anything other than a regular file (e.g. dir, symlink) just needs the header
if err := a.tw.WriteHeader(header); err != nil {
return fmt.Errorf("writing %q header: %w", pathInTar, err)
}
return nil
}
file, err := os.Open(pathInTar)
if err != nil {
// In case the file has been deleted since we last looked at it.
if os.IsNotExist(err) {
return nil
}
return err
}
defer func() {
_ = file.Close()
}()
// The size header must match the number of contents bytes.
//
// There is room for a race condition here if something writes to the file
// after we've read the file size.
//
// For small files, we avoid this by first copying the file into a buffer,
// and using the size of the buffer to populate the header.
//
// For larger files, we don't want to copy the whole thing into a buffer,
// because that would blow up heap size. There is some danger that this
// will lead to a spurious error when the tar writer validates the sizes.
// That error will be disruptive but will be handled as best as we
// can downstream.
useBuf := header.Size < 5000000
if useBuf {
a.copyBuf.Reset()
_, err = io.Copy(a.copyBuf, file)
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("copying %q: %w", pathInTar, err)
}
header.Size = int64(len(a.copyBuf.Bytes()))
}
// wait to write the header until _after_ the file is successfully opened
// to avoid generating an invalid tar entry that has a header but no contents
// in the case the file has been deleted
err = a.tw.WriteHeader(header)
if err != nil {
return fmt.Errorf("writing %q header: %w", pathInTar, err)
}
if useBuf {
_, err = io.Copy(a.tw, a.copyBuf)
} else {
_, err = io.Copy(a.tw, file)
}
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("copying %q: %w", pathInTar, err)
}
// explicitly flush so that if the entry is invalid we will detect it now and
// provide a more meaningful error
if err := a.tw.Flush(); err != nil {
return fmt.Errorf("finalizing %q: %w", pathInTar, err)
}
return nil
}
// entriesForPath writes the given source path into tarWriter at the given dest (recursively for directories).
// e.g. tarring my_dir --> dest d: d/file_a, d/file_b
// If source path does not exist, quietly skips it and returns no err
func (a *ArchiveBuilder) entriesForPath(localPath, containerPath string) ([]archiveEntry, error) {
localInfo, err := os.Stat(localPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
localPathIsDir := localInfo.IsDir()
if localPathIsDir {
// Make sure we can trim this off filenames to get valid relative filepaths
if !strings.HasSuffix(localPath, string(filepath.Separator)) {
localPath += string(filepath.Separator)
}
}
containerPath = strings.TrimPrefix(containerPath, "/")
result := make([]archiveEntry, 0)
err = filepath.Walk(localPath, func(curLocalPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("walking %q: %w", curLocalPath, err)
}
linkname := ""
if info.Mode()&os.ModeSymlink != 0 {
var err error
linkname, err = os.Readlink(curLocalPath)
if err != nil {
return err
}
}
var name string
//nolint:gocritic
if localPathIsDir {
// Name of file in tar should be relative to source directory...
tmp, err := filepath.Rel(localPath, curLocalPath)
if err != nil {
return fmt.Errorf("making %q relative to %q: %w", curLocalPath, localPath, err)
}
// ...and live inside `dest`
name = path.Join(containerPath, filepath.ToSlash(tmp))
} else if strings.HasSuffix(containerPath, "/") {
name = containerPath + filepath.Base(curLocalPath)
} else {
name = containerPath
}
header, err := archive.FileInfoHeader(name, info, linkname)
if err != nil {
// Not all types of files are allowed in a tarball. That's OK.
// Mimic the Docker behavior and just skip the file.
return nil
}
result = append(result, archiveEntry{
path: curLocalPath,
info: info,
header: header,
})
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func tarArchive(ops []PathMapping) io.ReadCloser {
pr, pw := io.Pipe()
go func() {
ab := NewArchiveBuilder(pw)
err := ab.ArchivePathsIfExist(ops)
if err != nil {
_ = pw.CloseWithError(fmt.Errorf("adding files to tar: %w", err))
} else {
// propagate errors from the TarWriter::Close() because it performs a final
// Flush() and any errors mean the tar is invalid
if err := ab.Close(); err != nil {
_ = pw.CloseWithError(fmt.Errorf("closing tar: %w", err))
} else {
_ = pw.Close()
}
}
}()
return pr
}
// Dedupe the entries with last-entry-wins semantics.
func dedupeEntries(entries []archiveEntry) []archiveEntry {
seenIndex := make(map[string]int, len(entries))
result := make([]archiveEntry, 0, len(entries))
for i, entry := range entries {
seenIndex[entry.header.Name] = i
}
for i, entry := range entries {
if seenIndex[entry.header.Name] == i {
result = append(result, entry)
}
}
return result
}
================================================
FILE: internal/tracing/attributes.go
================================================
/*
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 tracing
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/api/types/container"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// SpanOptions is a small helper type to make it easy to share the options helpers between
// downstream functions that accept slices of trace.SpanStartOption and trace.EventOption.
type SpanOptions []trace.SpanStartEventOption
type MetricsKey struct{}
type Metrics struct {
CountExtends int
CountIncludesLocal int
CountIncludesRemote int
}
func (s SpanOptions) SpanStartOptions() []trace.SpanStartOption {
out := make([]trace.SpanStartOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
func (s SpanOptions) EventOptions() []trace.EventOption {
out := make([]trace.EventOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
// ProjectOptions returns common attributes from a Compose project.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ProjectOptions(ctx context.Context, proj *types.Project) SpanOptions {
if proj == nil {
return nil
}
capabilities, gpu, tpu := proj.ServicesWithCapabilities()
attrs := []attribute.KeyValue{
attribute.String("project.name", proj.Name),
attribute.String("project.dir", proj.WorkingDir),
attribute.StringSlice("project.compose_files", proj.ComposeFiles),
attribute.StringSlice("project.profiles", proj.Profiles),
attribute.StringSlice("project.volumes", proj.VolumeNames()),
attribute.StringSlice("project.networks", proj.NetworkNames()),
attribute.StringSlice("project.secrets", proj.SecretNames()),
attribute.StringSlice("project.configs", proj.ConfigNames()),
attribute.StringSlice("project.models", proj.ModelNames()),
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
attribute.StringSlice("project.services.active", proj.ServiceNames()),
attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()),
attribute.StringSlice("project.services.build", proj.ServicesWithBuild()),
attribute.StringSlice("project.services.depends_on", proj.ServicesWithDependsOn()),
attribute.StringSlice("project.services.models", proj.ServicesWithModels()),
attribute.StringSlice("project.services.capabilities", capabilities),
attribute.StringSlice("project.services.capabilities.gpu", gpu),
attribute.StringSlice("project.services.capabilities.tpu", tpu),
}
if metrics, ok := ctx.Value(MetricsKey{}).(Metrics); ok {
attrs = append(attrs, attribute.Int("project.services.extends", metrics.CountExtends))
attrs = append(attrs, attribute.Int("project.includes.local", metrics.CountIncludesLocal))
attrs = append(attrs, attribute.Int("project.includes.remote", metrics.CountIncludesRemote))
}
if projHash, ok := projectHash(proj); ok {
attrs = append(attrs, attribute.String("project.hash", projHash))
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ServiceOptions returns common attributes from a Compose service.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ServiceOptions(service types.ServiceConfig) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("service.name", service.Name),
attribute.String("service.image", service.Image),
attribute.StringSlice("service.networks", keys(service.Networks)),
attribute.StringSlice("service.models", keys(service.Models)),
}
configNames := make([]string, len(service.Configs))
for i := range service.Configs {
configNames[i] = service.Configs[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.configs", configNames))
secretNames := make([]string, len(service.Secrets))
for i := range service.Secrets {
secretNames[i] = service.Secrets[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.secrets", secretNames))
volNames := make([]string, len(service.Volumes))
for i := range service.Volumes {
volNames[i] = service.Volumes[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.volumes", volNames))
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ContainerOptions returns common attributes from a Moby container.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ContainerOptions(ctr container.Summary) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("container.id", ctr.ID),
attribute.String("container.image", ctr.Image),
unixTimeAttr("container.created_at", ctr.Created),
}
if len(ctr.Names) != 0 {
attrs = append(attrs, attribute.String("container.name", strings.TrimPrefix(ctr.Names[0], "/")))
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
func keys[T any](m map[string]T) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func timeAttr(key string, value time.Time) attribute.KeyValue {
return attribute.String(key, value.Format(time.RFC3339))
}
func unixTimeAttr(key string, value int64) attribute.KeyValue {
return timeAttr(key, time.Unix(value, 0).UTC())
}
// projectHash returns a checksum from the JSON encoding of the project.
func projectHash(p *types.Project) (string, bool) {
if p == nil {
return "", false
}
// disabled services aren't included in the output, so make a copy with
// all the services active for hashing
var err error
p, err = p.WithServicesEnabled(append(p.ServiceNames(), p.DisabledServiceNames()...)...)
if err != nil {
return "", false
}
projData, err := json.Marshal(p)
if err != nil {
return "", false
}
d := sha256.Sum256(projData)
return fmt.Sprintf("%x", d), true
}
================================================
FILE: internal/tracing/attributes_test.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/stretchr/testify/require"
)
func TestProjectHash(t *testing.T) {
projA := &types.Project{
Name: "fake-proj",
WorkingDir: "/tmp",
Services: map[string]types.ServiceConfig{
"foo": {Image: "fake-image"},
},
DisabledServices: map[string]types.ServiceConfig{
"bar": {Image: "diff-image"},
},
}
projB := &types.Project{
Name: "fake-proj",
WorkingDir: "/tmp",
Services: map[string]types.ServiceConfig{
"foo": {Image: "fake-image"},
"bar": {Image: "diff-image"},
},
}
projC := &types.Project{
Name: "fake-proj",
WorkingDir: "/tmp",
Services: map[string]types.ServiceConfig{
"foo": {Image: "fake-image"},
"bar": {Image: "diff-image"},
"baz": {Image: "yet-another-image"},
},
}
hashA, ok := projectHash(projA)
require.True(t, ok)
require.NotEmpty(t, hashA)
hashB, ok := projectHash(projB)
require.True(t, ok)
require.NotEmpty(t, hashB)
require.Equal(t, hashA, hashB)
hashC, ok := projectHash(projC)
require.True(t, ok)
require.NotEmpty(t, hashC)
require.NotEqual(t, hashC, hashA)
}
================================================
FILE: internal/tracing/docker_context.go
================================================
/*
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 tracing
import (
"fmt"
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/docker/compose/v5/internal/memnet"
)
const otelConfigFieldName = "otel"
// traceClientFromDockerContext creates a gRPC OTLP client based on metadata
// from the active Docker CLI context.
func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptrace.Client, error) {
// attempt to extract an OTEL config from the Docker context to enable
// automatic integration with Docker Desktop;
cfg, err := ConfigFromDockerContext(dockerCli.ContextStore(), dockerCli.CurrentContext())
if err != nil {
return nil, fmt.Errorf("loading otel config from docker context metadata: %w", err)
}
if cfg.Endpoint == "" {
return nil, nil
}
// HACK: unfortunately _all_ public OTEL initialization functions
// implicitly read from the OS env, so temporarily unset them all and
// restore afterwards
defer func() {
for k, v := range otelEnv {
if err := os.Setenv(k, v); err != nil {
panic(fmt.Errorf("restoring env for %q: %w", k, err))
}
}
}()
for k := range otelEnv {
if err := os.Unsetenv(k); err != nil {
return nil, fmt.Errorf("stashing env for %q: %w", k, err)
}
}
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
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, fmt.Errorf("initializing otel connection from docker context metadata: %w", err)
}
client := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn))
return client, nil
}
// ConfigFromDockerContext inspects extra metadata included as part of the
// specified Docker context to try and extract a valid OTLP client configuration.
func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
meta, err := st.GetMetadata(name)
if err != nil {
return OTLPConfig{}, err
}
var otelCfg any
switch m := meta.Metadata.(type) {
case command.DockerContext:
otelCfg = m.AdditionalFields[otelConfigFieldName]
case map[string]any:
otelCfg = m[otelConfigFieldName]
}
if otelCfg == nil {
return OTLPConfig{}, nil
}
otelMap, ok := otelCfg.(map[string]any)
if !ok {
return OTLPConfig{}, fmt.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelConfigFieldName,
otelCfg,
otelMap,
)
}
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
cfg := OTLPConfig{
Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"),
}
return cfg, nil
}
// valueOrDefault returns the type-cast value at the specified key in the map
// if present and the correct type; otherwise, it returns the default value for
// T.
func valueOrDefault[T any](m map[string]any, key string) T {
if v, ok := m[key].(T); ok {
return v
}
return *new(T)
}
================================================
FILE: internal/tracing/errors.go
================================================
/*
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 tracing
import (
"go.opentelemetry.io/otel"
)
// skipErrors is a no-op otel.ErrorHandler.
type skipErrors struct{}
// Handle does nothing, ignoring any errors passed to it.
func (skipErrors) Handle(_ error) {}
var _ otel.ErrorHandler = skipErrors{}
================================================
FILE: internal/tracing/keyboard_metrics.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"context"
"go.opentelemetry.io/otel/attribute"
)
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive bool) {
commandAvailable := []string{}
if isDockerDesktopActive {
commandAvailable = append(commandAvailable, "gui")
commandAvailable = append(commandAvailable, "gui/composeview")
}
AddAttributeToSpan(ctx,
attribute.Bool("navmenu.enabled", enabled),
attribute.StringSlice("navmenu.command_available", commandAvailable))
}
================================================
FILE: internal/tracing/mux.go
================================================
/*
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 tracing
import (
"context"
"errors"
"sync"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
type MuxExporter struct {
exporters []sdktrace.SpanExporter
}
func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
var (
wg sync.WaitGroup
errMu sync.Mutex
errs = make([]error, 0, len(m.exporters))
)
for _, exporter := range m.exporters {
wg.Go(func() {
if err := exporter.ExportSpans(ctx, spans); err != nil {
errMu.Lock()
errs = append(errs, err)
errMu.Unlock()
}
})
}
wg.Wait()
return errors.Join(errs...)
}
func (m MuxExporter) Shutdown(ctx context.Context) error {
var (
wg sync.WaitGroup
errMu sync.Mutex
errs = make([]error, 0, len(m.exporters))
)
for _, exporter := range m.exporters {
wg.Go(func() {
if err := exporter.Shutdown(ctx); err != nil {
errMu.Lock()
errs = append(errs, err)
errMu.Unlock()
}
})
}
wg.Wait()
return errors.Join(errs...)
}
================================================
FILE: internal/tracing/tracing.go
================================================
/*
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 tracing
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli/command"
"github.com/moby/buildkit/util/tracing/detect"
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"github.com/docker/compose/v5/internal"
)
func init() {
detect.ServiceName = "compose"
// do not log tracing errors to stdio
otel.SetErrorHandler(skipErrors{})
}
// OTLPConfig contains the necessary values to initialize an OTLP client
// manually.
//
// This supports a minimal set of options based on what is necessary for
// automatic OTEL configuration from Docker context metadata.
type OTLPConfig struct {
Endpoint string
}
// ShutdownFunc flushes and stops an OTEL exporter.
type ShutdownFunc func(ctx context.Context) error
// envMap is a convenience type for OS environment variables.
type envMap map[string]string
func InitTracing(dockerCli command.Cli) (ShutdownFunc, error) {
// set global propagator to tracecontext (the default is no-op).
otel.SetTextMapPropagator(propagation.TraceContext{})
return InitProvider(dockerCli)
}
func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) {
ctx := context.Background()
var errs []error
var exporters []sdktrace.SpanExporter
envClient, otelEnv := traceClientFromEnv()
if envClient != nil {
if envExporter, err := otlptrace.New(ctx, envClient); err != nil {
errs = append(errs, err)
} else if envExporter != nil {
exporters = append(exporters, envExporter)
}
}
if dcClient, err := traceClientFromDockerContext(dockerCli, otelEnv); err != nil {
errs = append(errs, err)
} else if dcClient != nil {
if dcExporter, err := otlptrace.New(ctx, dcClient); err != nil {
errs = append(errs, err)
} else if dcExporter != nil {
exporters = append(exporters, dcExporter)
}
}
if len(errs) != 0 {
return nil, errors.Join(errs...)
}
res, err := resource.New(
ctx,
resource.WithAttributes(
semconv.ServiceName("compose"),
semconv.ServiceVersion(internal.Version),
attribute.String("docker.context", dockerCli.CurrentContext()),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
muxExporter := MuxExporter{exporters: exporters}
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithBatcher(muxExporter),
)
otel.SetTracerProvider(tracerProvider)
// Shutdown will flush any remaining spans and shut down the exporter.
return tracerProvider.Shutdown, nil
}
// traceClientFromEnv creates a GRPC OTLP client based on OS environment
// variables.
//
// https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
func traceClientFromEnv() (otlptrace.Client, envMap) {
hasOtelEndpointInEnv := false
otelEnv := make(map[string]string)
for _, kv := range os.Environ() {
k, v, ok := strings.Cut(kv, "=")
if !ok {
continue
}
if strings.HasPrefix(k, "OTEL_") {
otelEnv[k] = v
if strings.HasSuffix(k, "ENDPOINT") {
hasOtelEndpointInEnv = true
}
}
}
if !hasOtelEndpointInEnv {
return nil, nil
}
client := otlptracegrpc.NewClient()
return client, otelEnv
}
================================================
FILE: internal/tracing/tracing_test.go
================================================
/*
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 tracing_test
import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v5/internal/tracing"
)
var testStoreCfg = store.NewConfig(
func() any {
return &map[string]any{}
},
)
func TestExtractOtelFromContext(t *testing.T) {
if testing.Short() {
t.Skip("Requires filesystem access")
}
dir := t.TempDir()
st := store.New(dir, testStoreCfg)
err := st.CreateOrUpdate(store.Metadata{
Name: "test",
Metadata: command.DockerContext{
Description: t.Name(),
AdditionalFields: map[string]any{
"otel": map[string]any{
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:1234",
},
},
},
Endpoints: make(map[string]any),
})
require.NoError(t, err)
cfg, err := tracing.ConfigFromDockerContext(st, "test")
require.NoError(t, err)
require.Equal(t, "localhost:1234", cfg.Endpoint)
}
================================================
FILE: internal/tracing/wrap.go
================================================
/*
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 tracing
import (
"context"
"github.com/acarl005/stripansi"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
"go.opentelemetry.io/otel/trace"
)
// SpanWrapFunc wraps a function that takes a context with a trace.Span, marking the status as codes.Error if the
// wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFuncForErrGroup, except the latter is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFunc(spanName string, opts SpanOptions, fn func(ctx context.Context) error) func(context.Context) error {
return func(ctx context.Context) error {
ctx, span := otel.Tracer("").Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// SpanWrapFuncForErrGroup wraps a function that takes a context with a trace.Span, marking the status as codes.Error
// if the wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFunc, except this function is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFuncForErrGroup(ctx context.Context, spanName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
ctx, span := otel.Tracer("").Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// EventWrapFuncForErrGroup invokes a function and records an event, optionally including the returned
// error as the "exception message" on the event.
//
// This is intended for lightweight usage to wrap errgroup.Group calls where a full span is not desired.
func EventWrapFuncForErrGroup(ctx context.Context, eventName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
span := trace.SpanFromContext(ctx)
eventOpts := opts.EventOptions()
err := fn(ctx)
if err != nil {
eventOpts = append(eventOpts, trace.WithAttributes(semconv.ExceptionMessage(stripansi.Strip(err.Error()))))
}
span.AddEvent(eventName, eventOpts...)
return err
}
}
func AddAttributeToSpan(ctx context.Context, attr ...attribute.KeyValue) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(attr...)
}
================================================
FILE: internal/variables.go
================================================
/*
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 internal
// Version is the version of the CLI injected in compilation time
var Version = "dev"
================================================
FILE: pkg/api/api.go
================================================
/*
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 api
import (
"context"
"fmt"
"io"
"slices"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/volume"
)
// LoadListener receives events during project loading.
// Events include:
// - "extends": when a service extends another (metadata: service info)
// - "include": when including external compose files (metadata: {"path": StringList})
//
// Multiple listeners can be registered, and all will be notified of events.
type LoadListener func(event string, metadata map[string]any)
// ProjectLoadOptions configures how a Compose project should be loaded
type ProjectLoadOptions struct {
// ProjectName to use, or empty to infer from directory
ProjectName string
// ConfigPaths are paths to compose files
ConfigPaths []string
// WorkingDir is the project directory
WorkingDir string
// EnvFiles are paths to .env files
EnvFiles []string
// Profiles to activate
Profiles []string
// Services to select (empty = all)
Services []string
// Offline mode disables remote resource loading
Offline bool
// All includes all resources (not just those used by services)
All bool
// Compatibility enables v1 compatibility mode
Compatibility bool
// ProjectOptionsFns are compose-go project options to apply.
// Use cli.WithInterpolation(false), cli.WithNormalization(false), etc.
// This is optional - pass nil or empty slice to use defaults.
ProjectOptionsFns []cli.ProjectOptionsFn
// LoadListeners receive events during project loading.
// All registered listeners will be notified of events.
// This is optional - pass nil or empty slice if not needed.
LoadListeners []LoadListener
OCI OCIOptions
}
type OCIOptions struct {
InsecureRegistries []string
}
// Compose is the API interface one can use to programmatically use docker/compose in a third-party software
// Use [compose.NewComposeService] to get an actual instance
type Compose interface {
// Build executes the equivalent to a `compose build`
Build(ctx context.Context, project *types.Project, options BuildOptions) error
// Push executes the equivalent to a `compose push`
Push(ctx context.Context, project *types.Project, options PushOptions) error
// Pull executes the equivalent of a `compose pull`
Pull(ctx context.Context, project *types.Project, options PullOptions) error
// Create executes the equivalent to a `compose create`
Create(ctx context.Context, project *types.Project, options CreateOptions) error
// Start executes the equivalent to a `compose start`
Start(ctx context.Context, projectName string, options StartOptions) error
// Restart restarts containers
Restart(ctx context.Context, projectName string, options RestartOptions) error
// Stop executes the equivalent to a `compose stop`
Stop(ctx context.Context, projectName string, options StopOptions) error
// Up executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, options UpOptions) error
// Down executes the equivalent to a `compose down`
Down(ctx context.Context, projectName string, options DownOptions) error
// Logs executes the equivalent to a `compose logs`
Logs(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error
// Ps executes the equivalent to a `compose ps`
Ps(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error)
// List executes the equivalent to a `docker stack ls`
List(ctx context.Context, options ListOptions) ([]Stack, error)
// Kill executes the equivalent to a `compose kill`
Kill(ctx context.Context, projectName string, options KillOptions) error
// RunOneOffContainer creates a service oneoff container and starts its dependencies
RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
// Remove executes the equivalent to a `compose rm`
Remove(ctx context.Context, projectName string, options RemoveOptions) error
// Exec executes a command in a running service container
Exec(ctx context.Context, projectName string, options RunOptions) (int, error)
// Attach STDIN,STDOUT,STDERR to a running service container
Attach(ctx context.Context, projectName string, options AttachOptions) error
// Copy copies a file/folder between a service container and the local filesystem
Copy(ctx context.Context, projectName string, options CopyOptions) error
// Pause executes the equivalent to a `compose pause`
Pause(ctx context.Context, projectName string, options PauseOptions) error
// UnPause executes the equivalent to a `compose unpause`
UnPause(ctx context.Context, projectName string, options PauseOptions) error
// Top executes the equivalent to a `compose top`
Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error)
// Events executes the equivalent to a `compose events`
Events(ctx context.Context, projectName string, options EventsOptions) error
// Port executes the equivalent to a `compose port`
Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error)
// Publish executes the equivalent to a `compose publish`
Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) (map[string]ImageSummary, error)
// Watch services' development context and sync/notify/rebuild/restart on changes
Watch(ctx context.Context, project *types.Project, options WatchOptions) error
// Viz generates a graphviz graph of the project services
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
// Wait blocks until at least one of the services' container exits
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
// 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
// Create a new image from a service container's changes
Commit(ctx context.Context, projectName string, options CommitOptions) error
// Generate generates a Compose Project from existing containers
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
// Volumes executes the equivalent to a `docker volume ls`
Volumes(ctx context.Context, project string, options VolumesOptions) ([]VolumesSummary, error)
// LoadProject loads and validates a Compose project from configuration files.
LoadProject(ctx context.Context, options ProjectLoadOptions) (*types.Project, error)
}
type VolumesOptions struct {
Services []string
}
type VolumesSummary = volume.Volume
type ScaleOptions struct {
Services []string
}
type WaitOptions struct {
// Services passed in the command line to be waited
Services []string
// Executes a down when a container exits
DownProjectOnContainerExit bool
}
type VizOptions struct {
// IncludeNetworks if true, network names a container is attached to should appear in the graph node
IncludeNetworks bool
// IncludePorts if true, ports a container exposes should appear in the graph node
IncludePorts bool
// IncludeImageName if true, name of the image used to create a container should appear in the graph node
IncludeImageName bool
// Indentation string to be used to indent graphviz code, e.g. "\t", " "
Indentation string
}
// WatchLogger is a reserved name to log watch events
const WatchLogger = "#watch"
// WatchOptions group options of the Watch API
type WatchOptions struct {
Build *BuildOptions
LogTo LogConsumer
Prune bool
Services []string
}
// BuildOptions group options of the Build API
type BuildOptions struct {
// Pull always attempt to pull a newer version of the image
Pull bool
// Push pushes service images
Push bool
// Progress set type of progress output ("auto", "plain", "tty")
Progress string
// Args set build-time args
Args types.MappingWithEquals
// NoCache disables cache use
NoCache bool
// Quiet make the build process not output to the console
Quiet bool
// Services passed in the command line to be built
Services []string
// Deps also build selected services dependencies
Deps bool
// Ssh authentications passed in the command line
SSHs []types.SSHKey
// Memory limit for the build container
Memory int64
// Builder name passed in the command line
Builder string
// Print don't actually run builder but print equivalent build config
Print bool
// Check let builder validate build configuration
Check bool
// Attestations allows to enable attestations generation
Attestations bool
// Provenance generate a provenance attestation
Provenance string
// SBOM generate a SBOM attestation
SBOM string
// Out is the stream to write build progress
Out io.Writer
}
// Apply mutates project according to build options
func (o BuildOptions) Apply(project *types.Project) error {
platform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
for name, service := range project.Services {
if service.Provider == nil && service.Image == "" && service.Build == nil {
return fmt.Errorf("invalid service %q. Must specify either image or build", name)
}
if service.Build == nil {
continue
}
if platform != "" {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, platform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, platform)
}
service.Platform = platform
}
if service.Platform != "" {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
}
}
service.Build.Pull = service.Build.Pull || o.Pull
service.Build.NoCache = service.Build.NoCache || o.NoCache
project.Services[name] = service
}
return nil
}
// CreateOptions group options of the Create API
type CreateOptions struct {
Build *BuildOptions
// Services defines the services user interacts with
Services []string
// Remove legacy containers for services that are not defined in the project
RemoveOrphans bool
// Ignore legacy containers for services that are not defined in the project
IgnoreOrphans bool
// Recreate define the strategy to apply on existing containers
Recreate string
// RecreateDependencies define the strategy to apply on dependencies services
RecreateDependencies string
// Inherit reuse anonymous volumes from previous container
Inherit bool
// Timeout set delay to wait for container to gracefully stop before sending SIGKILL
Timeout *time.Duration
// QuietPull makes the pulling process quiet
QuietPull bool
}
// StartOptions group options of the Start API
type StartOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
// Attach to container and forward logs if not nil
Attach LogConsumer
// AttachTo set the services to attach to
AttachTo []string
// OnExit defines behavior when a container stops
OnExit Cascade
// ExitCodeFrom return exit code from specified service
ExitCodeFrom string
// Wait won't return until containers reached the running|healthy state
Wait bool
WaitTimeout time.Duration
// Services passed in the command line to be started
Services []string
Watch bool
NavigationMenu bool
}
type Cascade int
const (
CascadeIgnore Cascade = iota
CascadeStop Cascade = iota
CascadeFail Cascade = iota
)
// RestartOptions group options of the Restart API
type RestartOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
// Timeout override container restart timeout
Timeout *time.Duration
// Services passed in the command line to be restarted
Services []string
// NoDeps ignores services dependencies
NoDeps bool
}
// StopOptions group options of the Stop API
type StopOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
// Timeout override container stop timeout
Timeout *time.Duration
// Services passed in the command line to be stopped
Services []string
}
// UpOptions group options of the Up API
type UpOptions struct {
Create CreateOptions
Start StartOptions
}
// DownOptions group options of the Down API
type DownOptions struct {
// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
RemoveOrphans bool
// Project is the compose project used to define this app. Might be nil if user ran `down` just with project name
Project *types.Project
// Timeout override container stop timeout
Timeout *time.Duration
// Images remove image used by services. 'all': Remove all images. 'local': Remove only images that don't have a tag
Images string
// Volumes remove volumes, both declared in the `volumes` section and anonymous ones
Volumes bool
// Services passed in the command line to be stopped
Services []string
}
// ConfigOptions group options of the Config API
type ConfigOptions struct {
// Format define the output format used to dump converted application model (json|yaml)
Format string
// Output defines the path to save the application model
Output string
// Resolve image reference to digests
ResolveImageDigests bool
}
// PushOptions group options of the Push API
type PushOptions struct {
Quiet bool
IgnoreFailures bool
ImageMandatory bool
}
// PullOptions group options of the Pull API
type PullOptions struct {
Quiet bool
IgnoreFailures bool
IgnoreBuildable bool
}
// ImagesOptions group options of the Images API
type ImagesOptions struct {
Services []string
}
// KillOptions group options of the Kill API
type KillOptions struct {
// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
RemoveOrphans bool
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
// Services passed in the command line to be killed
Services []string
// Signal to send to containers
Signal string
// All can be set to true to try to kill all found containers, independently of their state
All bool
}
// RemoveOptions group options of the Remove API
type RemoveOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
// Stop option passed in the command line
Stop bool
// Volumes remove anonymous volumes
Volumes bool
// Force don't ask to confirm removal
Force bool
// Services passed in the command line to be removed
Services []string
}
// RunOptions group options of the Run API
type RunOptions struct {
CreateOptions
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
Name string
Service string
Command []string
Entrypoint []string
Detach bool
AutoRemove bool
Tty bool
Interactive bool
WorkingDir string
User string
Environment []string
CapAdd []string
CapDrop []string
Labels types.Labels
Privileged bool
UseNetworkAliases bool
NoDeps bool
// used by exec
Index int
}
// AttachOptions group options of the Attach API
type AttachOptions struct {
Project *types.Project
Service string
Index int
DetachKeys string
NoStdin bool
Proxy bool
}
// EventsOptions group options of the Events API
type EventsOptions struct {
Services []string
Consumer func(event Event) error
Since string
Until string
}
// Event is a container runtime event served by Events API
type Event struct {
Timestamp time.Time
Service string
Container string
Status string
Attributes map[string]string
}
// PortOptions group options of the Port API
type PortOptions struct {
Protocol string
Index int
}
// OCIVersion controls manifest generation to ensure compatibility
// with different registries.
//
// Currently, this is not exposed as an option to the user – Compose uses
// OCI 1.0 mode automatically for ECR registries based on domain and OCI 1.1
// for all other registries.
//
// There are likely other popular registries that do not support the OCI 1.1
// format, so it might make sense to expose this as a CLI flag or see if
// there's a way to generically probe the registry for support level.
type OCIVersion string
const (
OCIVersion1_0 OCIVersion = "1.0"
OCIVersion1_1 OCIVersion = "1.1"
)
// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool
Application bool
WithEnvironment bool
OCIVersion OCIVersion
// Use plain HTTP to access registry. Should only be used for testing purpose
InsecureRegistry bool
}
func (e Event) String() string {
t := e.Timestamp.Format("2006-01-02 15:04:05.000000")
var attr []string
for k, v := range e.Attributes {
attr = append(attr, fmt.Sprintf("%s=%s", k, v))
}
return fmt.Sprintf("%s container %s %s (%s)\n", t, e.Status, e.Container, strings.Join(attr, ", "))
}
// ListOptions group options of the ls API
type ListOptions struct {
All bool
}
// PsOptions group options of the Ps API
type PsOptions struct {
Project *types.Project
All bool
Services []string
}
// CopyOptions group options of the cp API
type CopyOptions struct {
Source string
Destination string
All bool
Index int
FollowLink bool
CopyUIDGID bool
}
// PortPublisher hold status about published port
type PortPublisher struct {
URL string
TargetPort int
PublishedPort int
Protocol string
}
// ContainerSummary hold high-level description of a container
type ContainerSummary struct {
ID string
Name string
Names []string
Image string
Command string
Project string
Service string
Created int64
State container.ContainerState
Status string
Health container.HealthStatus
ExitCode int
Publishers PortPublishers
Labels map[string]string
SizeRw int64 `json:",omitempty"`
SizeRootFs int64 `json:",omitempty"`
Mounts []string
Networks []string
LocalVolumes int
}
// PortPublishers is a slice of PortPublisher
type PortPublishers []PortPublisher
// Len implements sort.Interface
func (p PortPublishers) Len() int {
return len(p)
}
// Less implements sort.Interface
func (p PortPublishers) Less(i, j int) bool {
left := p[i]
right := p[j]
if left.URL != right.URL {
return left.URL < right.URL
}
if left.TargetPort != right.TargetPort {
return left.TargetPort < right.TargetPort
}
if left.PublishedPort != right.PublishedPort {
return left.PublishedPort < right.PublishedPort
}
return left.Protocol < right.Protocol
}
// Swap implements sort.Interface
func (p PortPublishers) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
// ContainerProcSummary holds container processes top data
type ContainerProcSummary struct {
ID string
Name string
Processes [][]string
Titles []string
Service string
Replica string
}
// ImageSummary holds container image description
type ImageSummary struct {
ID string
Repository string
Tag string
Platform platforms.Platform
Size int64
Created *time.Time
LastTagTime time.Time
}
// ServiceStatus hold status about a service
type ServiceStatus struct {
ID string
Name string
Replicas int
Desired int
Ports []string
Publishers []PortPublisher
}
// LogOptions defines optional parameters for the `Log` API
type LogOptions struct {
Project *types.Project
Index int
Services []string
Tail string
Since string
Until string
Follow bool
Timestamps bool
}
// PauseOptions group options of the Pause API
type PauseOptions struct {
// Services passed in the command line to be started
Services []string
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
}
// ExportOptions group options of the Export API
type ExportOptions struct {
Service string
Index int
Output string
}
// CommitOptions group options of the Commit API
type CommitOptions struct {
Service string
Reference string
Pause bool
Comment string
Author string
Changes opts.ListOpts
Index int
}
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"
// RUNNING indicates that stack is deployed and services are running
RUNNING string = "Running"
// UPDATING indicates that some stack resources are being recreated
UPDATING string = "Updating"
// REMOVING indicates that stack is being deleted
REMOVING string = "Removing"
// UNKNOWN indicates unknown stack state
UNKNOWN string = "Unknown"
// FAILED indicates that stack deployment failed
FAILED string = "Failed"
)
const (
// RecreateDiverged to recreate services which configuration diverges from compose model
RecreateDiverged = "diverged"
// RecreateForce to force service container being recreated
RecreateForce = "force"
// RecreateNever to never recreate existing service containers
RecreateNever = "never"
)
// Stack holds the name and state of a compose application/stack
type Stack struct {
ID string
Name string
Status string
ConfigFiles string
Reason string
}
// LogConsumer is a callback to process log messages from services
type LogConsumer interface {
Log(containerName, message string)
Err(containerName, message string)
Status(container, msg string)
}
// ContainerEventListener is a callback to process ContainerEvent from services
type ContainerEventListener func(event ContainerEvent)
// ContainerEvent notify an event has been collected on source container implementing Service
type ContainerEvent struct {
Type int
Time int64
Container *ContainerSummary
// Source is the name of the container _without the project prefix_.
//
// This is only suitable for display purposes within Compose, as it's
// not guaranteed to be unique across services.
Source string
ID string
Service string
Line string
// ExitCode is only set on ContainerEventExited events
ExitCode int
Restarting bool
}
const (
// ContainerEventLog is a ContainerEvent of type log on stdout. Line is set
ContainerEventLog = iota
// ContainerEventErr is a ContainerEvent of type log on stderr. Line is set
ContainerEventErr
// ContainerEventStarted let consumer know a container has been started
ContainerEventStarted
// ContainerEventRestarted let consumer know a container has been restarted
ContainerEventRestarted
// ContainerEventStopped is a ContainerEvent of type stopped.
ContainerEventStopped
// ContainerEventCreated let consumer know a new container has been created
ContainerEventCreated
// ContainerEventRecreated let consumer know container stopped but his being replaced
ContainerEventRecreated
// ContainerEventExited is a ContainerEvent of type exit. ExitCode is set
ContainerEventExited
// UserCancel user canceled compose up, we are stopping containers
HookEventLog
)
// Separator is used for naming components
var Separator = "-"
// GetImageNameOrDefault computes the default image name for a service, used to tag built images
func GetImageNameOrDefault(service types.ServiceConfig, projectName string) string {
imageName := service.Image
if imageName == "" {
imageName = projectName + Separator + service.Name
}
return imageName
}
================================================
FILE: pkg/api/api_test.go
================================================
/*
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 api
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestRunOptionsEnvironmentMap(t *testing.T) {
opts := RunOptions{
Environment: []string{
"FOO=BAR",
"ZOT=",
"QIX",
},
}
env := types.NewMappingWithEquals(opts.Environment)
assert.Equal(t, *env["FOO"], "BAR")
assert.Equal(t, *env["ZOT"], "")
assert.Check(t, env["QIX"] == nil)
}
================================================
FILE: pkg/api/context.go
================================================
/*
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 api
// ContextInfo provides Docker context information for advanced scenarios
type ContextInfo interface {
// CurrentContext returns the name of the current Docker context
// Returns "default" for simple clients without context support
CurrentContext() string
// ServerOSType returns the Docker daemon's operating system (linux/windows/darwin)
// Used for OS-specific compatibility checks
ServerOSType() string
// BuildKitEnabled determines whether BuildKit should be used for builds
// Checks DOCKER_BUILDKIT env var, config, and daemon capabilities
BuildKitEnabled() (bool, error)
}
================================================
FILE: pkg/api/env.go
================================================
/*
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 api
// ComposeCompatibility try to mimic compose v1 as much as possible
const ComposeCompatibility = "COMPOSE_COMPATIBILITY"
================================================
FILE: pkg/api/errors.go
================================================
/*
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 api
import (
"errors"
)
const (
// ExitCodeLoginRequired exit code when command cannot execute because it requires cloud login
// This will be used by VSCode to detect when creating context if the user needs to login first
ExitCodeLoginRequired = 5
)
var (
// ErrNotFound is returned when an object is not found
ErrNotFound = errors.New("not found")
// ErrAlreadyExists is returned when an object already exists
ErrAlreadyExists = errors.New("already exists")
// ErrForbidden is returned when an operation is not permitted
ErrForbidden = errors.New("forbidden")
// ErrUnknown is returned when the error type is unmapped
ErrUnknown = errors.New("unknown")
// ErrNotImplemented is returned when a backend doesn't implement an action
ErrNotImplemented = errors.New("not implemented")
// ErrUnsupportedFlag is returned when a backend doesn't support a flag
ErrUnsupportedFlag = errors.New("unsupported flag")
// ErrCanceled is returned when the command was canceled by user
ErrCanceled = errors.New("canceled")
// ErrParsingFailed is returned when a string cannot be parsed
ErrParsingFailed = errors.New("parsing failed")
// ErrNoResources is returned when operation didn't selected any resource
ErrNoResources = errors.New("no resources")
)
// IsNotFoundError returns true if the unwrapped error is ErrNotFound
func IsNotFoundError(err error) bool {
return errors.Is(err, ErrNotFound)
}
// IsAlreadyExistsError returns true if the unwrapped error is ErrAlreadyExists
func IsAlreadyExistsError(err error) bool {
return errors.Is(err, ErrAlreadyExists)
}
// IsForbiddenError returns true if the unwrapped error is ErrForbidden
func IsForbiddenError(err error) bool {
return errors.Is(err, ErrForbidden)
}
// IsUnknownError returns true if the unwrapped error is ErrUnknown
func IsUnknownError(err error) bool {
return errors.Is(err, ErrUnknown)
}
// IsErrUnsupportedFlag returns true if the unwrapped error is ErrUnsupportedFlag
func IsErrUnsupportedFlag(err error) bool {
return errors.Is(err, ErrUnsupportedFlag)
}
// IsErrNotImplemented returns true if the unwrapped error is ErrNotImplemented
func IsErrNotImplemented(err error) bool {
return errors.Is(err, ErrNotImplemented)
}
// IsErrParsingFailed returns true if the unwrapped error is ErrParsingFailed
func IsErrParsingFailed(err error) bool {
return errors.Is(err, ErrParsingFailed)
}
// IsErrCanceled returns true if the unwrapped error is ErrCanceled
func IsErrCanceled(err error) bool {
return errors.Is(err, ErrCanceled)
}
================================================
FILE: pkg/api/errors_test.go
================================================
/*
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 api
import (
"errors"
"fmt"
"testing"
"gotest.tools/v3/assert"
)
func TestIsNotFound(t *testing.T) {
err := fmt.Errorf(`object "name": %w`, ErrNotFound)
assert.Assert(t, IsNotFoundError(err))
assert.Assert(t, !IsNotFoundError(errors.New("another error")))
}
func TestIsAlreadyExists(t *testing.T) {
err := fmt.Errorf(`object "name": %w`, ErrAlreadyExists)
assert.Assert(t, IsAlreadyExistsError(err))
assert.Assert(t, !IsAlreadyExistsError(errors.New("another error")))
}
func TestIsForbidden(t *testing.T) {
err := fmt.Errorf(`object "name": %w`, ErrForbidden)
assert.Assert(t, IsForbiddenError(err))
assert.Assert(t, !IsForbiddenError(errors.New("another error")))
}
func TestIsUnknown(t *testing.T) {
err := fmt.Errorf(`object "name": %w`, ErrUnknown)
assert.Assert(t, IsUnknownError(err))
assert.Assert(t, !IsUnknownError(errors.New("another error")))
}
================================================
FILE: pkg/api/event.go
================================================
/*
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 api
import (
"context"
)
// EventStatus indicates the status of an action
type EventStatus int
const (
// Working means that the current task is working
Working EventStatus = iota
// Done means that the current task is done
Done
// Warning means that the current task has warning
Warning
// Error means that the current task has errored
Error
)
// ResourceCompose is a special resource ID used when event applies to all resources in the application
const ResourceCompose = "Compose"
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
StatusDownloading = "Downloading"
StatusDownloadComplete = "Download complete"
StatusConfiguring = "Configuring"
StatusConfigured = "Configured"
)
// Resource represents status change and progress for a compose resource.
type Resource struct {
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
func (e *Resource) StatusText() string {
switch e.Status {
case Working:
return "Working"
case Warning:
return "Warning"
case Done:
return "Done"
default:
return "Error"
}
}
// EventProcessor is notified about Compose operations and tasks
type EventProcessor interface {
// Start is triggered as a Compose operation is starting with context
Start(ctx context.Context, operation string)
// On notify about (sub)task and progress processing operation
On(events ...Resource)
// Done is triggered as a Compose operation completed
Done(operation string, success bool)
}
================================================
FILE: pkg/api/labels.go
================================================
/*
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 api
import (
"github.com/hashicorp/go-version"
"github.com/docker/compose/v5/internal"
)
const (
// ProjectLabel allow to track resource related to a compose project
ProjectLabel = "com.docker.compose.project"
// ServiceLabel allow to track resource related to a compose service
ServiceLabel = "com.docker.compose.service"
// ConfigHashLabel stores configuration hash for a compose service
ConfigHashLabel = "com.docker.compose.config-hash"
// ContainerNumberLabel stores the container index of a replicated service
ContainerNumberLabel = "com.docker.compose.container-number"
// VolumeLabel allow to track resource related to a compose volume
VolumeLabel = "com.docker.compose.volume"
// NetworkLabel allow to track resource related to a compose network
NetworkLabel = "com.docker.compose.network"
// WorkingDirLabel stores absolute path to compose project working directory
WorkingDirLabel = "com.docker.compose.project.working_dir"
// ConfigFilesLabel stores absolute path to compose project configuration files
ConfigFilesLabel = "com.docker.compose.project.config_files"
// EnvironmentFileLabel stores absolute path to compose project env file set by `--env-file`
EnvironmentFileLabel = "com.docker.compose.project.environment_file"
// OneoffLabel stores value 'True' for one-off containers created by `compose run`
OneoffLabel = "com.docker.compose.oneoff"
// SlugLabel stores unique slug used for one-off container identity
SlugLabel = "com.docker.compose.slug"
// ImageDigestLabel stores digest of the container image used to run service
ImageDigestLabel = "com.docker.compose.image"
// DependenciesLabel stores service dependencies
DependenciesLabel = "com.docker.compose.depends_on"
// VersionLabel stores the compose tool version used to build/run application
VersionLabel = "com.docker.compose.version"
// ImageBuilderLabel stores the builder (classic or BuildKit) used to produce the image.
ImageBuilderLabel = "com.docker.compose.image.builder"
// ContainerReplaceLabel is set when container is created to replace another container (recreated)
ContainerReplaceLabel = "com.docker.compose.replace"
)
// ComposeVersion is the compose tool version as declared by label VersionLabel
var ComposeVersion string
func init() {
v, err := version.NewVersion(internal.Version)
if err == nil {
ComposeVersion = v.Core().String()
}
}
================================================
FILE: pkg/api/labels_test.go
================================================
/*
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 api
import (
"testing"
"github.com/hashicorp/go-version"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/internal"
)
func TestComposeVersionInitialization(t *testing.T) {
v, err := version.NewVersion(internal.Version)
if err != nil {
assert.Equal(t, "", ComposeVersion, "ComposeVersion should be empty for a non-semver internal version (e.g. 'devel')")
} else {
expected := v.Core().String()
assert.Equal(t, expected, ComposeVersion, "ComposeVersion should be the core of internal.Version")
}
}
================================================
FILE: pkg/bridge/convert.go
================================================
/*
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 bridge
import (
"context"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/command"
cli "github.com/docker/cli/cli/command/container"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
"github.com/sirupsen/logrus"
"go.yaml.in/yaml/v4"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
type ConvertOptions struct {
Output string
Templates string
Transformations []string
}
func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
if len(opts.Transformations) == 0 {
opts.Transformations = []string{DefaultTransformerImage}
}
// Load image references, secrets and configs, also expose ports
project, err := LoadAdditionalResources(ctx, dockerCli, project)
if err != nil {
return err
}
// for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
raw, err := project.MarshalYAML(types.WithSecretContent)
// Marshall to YAML
if err != nil {
return fmt.Errorf("cannot render project into yaml: %w", err)
}
var model map[string]any
err = yaml.Unmarshal(raw, &model)
if err != nil {
return fmt.Errorf("cannot render project into yaml: %w", err)
}
if opts.Output != "" {
_ = os.RemoveAll(opts.Output)
err := os.MkdirAll(opts.Output, 0o744)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("cannot create output folder: %w", err)
}
}
// Run Transformers images
return convert(ctx, dockerCli, model, opts)
}
func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
raw, err := yaml.Marshal(model)
if err != nil {
return err
}
dir, err := os.MkdirTemp("", "compose-convert-*")
if err != nil {
return err
}
defer func() {
err := os.RemoveAll(dir)
if err != nil {
logrus.Warnf("failed to remove temp dir %s: %v", dir, err)
}
}()
composeYaml := filepath.Join(dir, "compose.yaml")
err = os.WriteFile(composeYaml, raw, 0o600)
if err != nil {
return err
}
out, err := filepath.Abs(opts.Output)
if err != nil {
return err
}
binds := []string{
fmt.Sprintf("%s:%s", dir, "/in"),
fmt.Sprintf("%s:%s", out, "/out"),
}
if opts.Templates != "" {
templateDir, err := filepath.Abs(opts.Templates)
if err != nil {
return err
}
binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
}
for _, transformation := range opts.Transformations {
_, err = inspectWithPull(ctx, dockerCli, transformation)
if err != nil {
return err
}
containerConfig := &container.Config{
Image: transformation,
Env: []string{"LICENSE_AGREEMENT=true"},
}
// On POSIX systems, this is a decimal number representing the uid.
// On Windows, this is a security identifier (SID) in a string format and the engine isn't able to manage it
if runtime.GOOS != "windows" {
usr, err := user.Current()
if err != nil {
return err
}
containerConfig.User = usr.Uid
}
created, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
Config: containerConfig,
HostConfig: &container.HostConfig{
Binds: binds,
AutoRemove: true,
},
NetworkingConfig: &network.NetworkingConfig{},
})
if err != nil {
return err
}
err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
Attach: true,
Containers: []string{created.ID},
})
if err != nil {
return err
}
}
return nil
}
// LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
func LoadAdditionalResources(ctx context.Context, dockerCLI command.Cli, project *types.Project) (*types.Project, error) {
for name, service := range project.Services {
imageName := api.GetImageNameOrDefault(service, project.Name)
inspect, err := inspectWithPull(ctx, dockerCLI, imageName)
if err != nil {
return nil, err
}
service.Image = imageName
exposed := utils.Set[string]{}
exposed.AddAll(service.Expose...)
for port := range inspect.Config.ExposedPorts {
p, err := network.ParsePort(port)
if err != nil {
return nil, err
}
exposed.Add(strconv.Itoa(int(p.Num())))
}
for _, port := range service.Ports {
exposed.Add(strconv.Itoa(int(port.Target)))
}
service.Expose = exposed.Elements()
project.Services[name] = service
}
for name, secret := range project.Secrets {
f, err := loadFileObject(types.FileObjectConfig(secret))
if err != nil {
return nil, err
}
project.Secrets[name] = types.SecretConfig(f)
}
for name, config := range project.Configs {
f, err := loadFileObject(types.FileObjectConfig(config))
if err != nil {
return nil, err
}
project.Configs[name] = types.ConfigObjConfig(f)
}
return project, nil
}
func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
if !conf.External {
switch {
case conf.Environment != "":
conf.Content = os.Getenv(conf.Environment)
case conf.File != "":
bytes, err := os.ReadFile(conf.File)
if err != nil {
return conf, err
}
conf.Content = string(bytes)
}
}
return conf, nil
}
func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
if errdefs.IsNotFound(err) {
var stream io.ReadCloser
stream, err = dockerCli.Client().ImagePull(ctx, imageName, client.ImagePullOptions{})
if err != nil {
return image.InspectResponse{}, err
}
defer func() { _ = stream.Close() }()
out := dockerCli.Out()
err = jsonmessage.DisplayJSONMessagesStream(stream, out, out.FD(), out.IsTerminal(), nil)
if err != nil {
return image.InspectResponse{}, err
}
if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
return image.InspectResponse{}, err
}
}
return inspect.InspectResponse, err
}
================================================
FILE: pkg/bridge/transformers.go
================================================
/*
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 bridge
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/docker/cli/cli/command"
"github.com/moby/go-archive"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
)
const (
TransformerLabel = "com.docker.compose.bridge"
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
templatesPath = "/templates"
)
type CreateTransformerOptions struct {
Dest string
From string
}
func CreateTransformer(ctx context.Context, dockerCli command.Cli, options CreateTransformerOptions) error {
if options.From == "" {
options.From = DefaultTransformerImage
}
out, err := filepath.Abs(options.Dest)
if err != nil {
return err
}
if _, err := os.Stat(out); err == nil {
return fmt.Errorf("output folder %s already exists", out)
}
tmpl := filepath.Join(out, "templates")
err = os.MkdirAll(tmpl, 0o744)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("cannot create output folder: %w", err)
}
if err := command.ValidateOutputPath(out); err != nil {
return err
}
created, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
Image: options.From,
})
defer func() {
_, _ = dockerCli.Client().ContainerRemove(context.Background(), created.ID, client.ContainerRemoveOptions{
Force: true,
})
}()
if err != nil {
return err
}
resp, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, client.CopyFromContainerOptions{
SourcePath: templatesPath,
})
if err != nil {
return err
}
defer func() {
_ = resp.Content.Close()
}()
srcInfo := archive.CopyInfo{
Path: templatesPath,
Exists: true,
IsDir: resp.Stat.Mode.IsDir(),
}
preArchive := resp.Content
if srcInfo.RebaseName != "" {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(resp.Content, srcBase, srcInfo.RebaseName)
}
if err := archive.CopyTo(preArchive, srcInfo, out); err != nil {
return err
}
dockerfile := `FROM docker/compose-bridge-transformer
LABEL com.docker.compose.bridge=transformation
COPY templates /templates
`
if err := os.WriteFile(filepath.Join(out, "Dockerfile"), []byte(dockerfile), 0o700); err != nil {
return err
}
_, err = fmt.Fprintf(dockerCli.Out(), "Transformer created in %q\n", out)
return err
}
func ListTransformers(ctx context.Context, dockerCli command.Cli) ([]image.Summary, error) {
res, err := dockerCli.Client().ImageList(ctx, client.ImageListOptions{
Filters: make(client.Filters).Add("label", fmt.Sprintf("%s=%s", TransformerLabel, "transformation")),
})
if err != nil {
return nil, err
}
return res.Items, nil
}
================================================
FILE: pkg/compose/apiSocket.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"errors"
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
)
// --use-api-socket is not actually supported by the Docker Engine
// but is a client-side hack (see https://github.com/docker/cli/blob/master/cli/command/container/create.go#L246)
// we replicate here by transforming the project model
func (s *composeService) useAPISocket(project *types.Project) (*types.Project, error) {
useAPISocket := false
for _, service := range project.Services {
if service.UseAPISocket {
useAPISocket = true
break
}
}
if !useAPISocket {
return project, nil
}
if s.getContextInfo().ServerOSType() == "windows" {
return nil, errors.New("use_api_socket can't be used with a Windows Docker Engine")
}
creds, err := s.configFile().GetAllCredentials()
if err != nil {
return nil, fmt.Errorf("resolving credentials failed: %w", err)
}
newConfig := &configfile.ConfigFile{
AuthConfigs: creds,
}
var configBuf bytes.Buffer
if err := newConfig.SaveToWriter(&configBuf); err != nil {
return nil, fmt.Errorf("saving creds for API socket: %w", err)
}
project.Configs["#apisocket"] = types.ConfigObjConfig{
Content: configBuf.String(),
}
for name, service := range project.Services {
if !service.UseAPISocket {
continue
}
service.Volumes = append(service.Volumes, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
})
_, envvarPresent := service.Environment["DOCKER_CONFIG"]
// If the DOCKER_CONFIG env var is already present, we assume the client knows
// what they're doing and don't inject the creds.
if !envvarPresent {
// Set our special little location for the config file.
path := "/run/secrets/docker"
service.Environment["DOCKER_CONFIG"] = &path
}
service.Configs = append(service.Configs, types.ServiceConfigObjConfig{
Source: "#apisocket",
Target: "/run/secrets/docker/config.json",
})
project.Services[name] = service
}
return project, nil
}
================================================
FILE: pkg/compose/api_versions.go
================================================
/*
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
// Docker Engine API version constants.
// These versions correspond to specific Docker Engine releases and their features.
const (
// apiVersion148 represents Docker Engine API version 1.48 (Engine v28.0).
//
// New features in this version:
// - Volume mounts with type=image support
//
// Before this version:
// - Only bind, volume, and tmpfs mount types were supported
apiVersion148 = "1.48"
// apiVersion149 represents Docker Engine API version 1.49 (Engine v28.1).
//
// New features in this version:
// - Network interface_name configuration
// - Platform parameter in ImageList API
//
// Before this version:
// - interface_name was not configurable
// - ImageList didn't support platform filtering
apiVersion149 = "1.49"
)
// Docker Engine version strings for user-facing error messages.
// These should be used in error messages to provide clear version requirements.
const (
// dockerEngineV28 is the major version string for Docker Engine 28.x
dockerEngineV28 = "v28"
// DockerEngineV28_1 is the specific version string for Docker Engine 28.1
DockerEngineV28_1 = "v28.1"
)
// Build tool version constants
const (
// buildxMinVersion is the minimum required version of buildx for compose build
buildxMinVersion = "0.17.0"
)
================================================
FILE: pkg/compose/attach.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/api/pkg/stdcopy"
containerType "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func (s *composeService) attach(ctx context.Context, project *types.Project, listener api.ContainerEventListener, selectedServices []string) (Containers, error) {
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, selectedServices...)
if err != nil {
return nil, err
}
if len(containers) == 0 {
return containers, nil
}
containers.sorted() // This enforces predictable colors assignment
var names []string
for _, c := range containers {
names = append(names, getContainerNameWithoutProject(c))
}
_, err = fmt.Fprintf(s.stdout(), "Attaching to %s\n", strings.Join(names, ", "))
if err != nil {
logrus.Debugf("failed to write attach message: %v", err)
}
for _, ctr := range containers {
err := s.attachContainer(ctx, ctr, listener)
if err != nil {
return nil, err
}
}
return containers, nil
}
func (s *composeService) attachContainer(ctx context.Context, container containerType.Summary, listener api.ContainerEventListener) error {
service := container.Labels[api.ServiceLabel]
name := getContainerNameWithoutProject(container)
return s.doAttachContainer(ctx, service, container.ID, name, listener)
}
func (s *composeService) doAttachContainer(ctx context.Context, service, id, name string, listener api.ContainerEventListener) error {
inspect, err := s.apiClient().ContainerInspect(ctx, id, client.ContainerInspectOptions{})
if err != nil {
return err
}
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventLog,
Source: name,
ID: id,
Service: service,
Line: line,
})
})
wErr := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventErr,
Source: name,
ID: id,
Service: service,
Line: line,
})
})
err = s.attachContainerStreams(ctx, id, inspect.Container.Config.Tty, wOut, wErr)
if err != nil {
return err
}
return nil
}
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdout, stderr io.WriteCloser) error {
streamOut, err := s.getContainerStreams(ctx, container)
if err != nil {
return err
}
if stdout != nil {
go func() {
defer func() {
if err := stdout.Close(); err != nil {
logrus.Debugf("failed to close stdout: %v", err)
}
if err := stderr.Close(); err != nil {
logrus.Debugf("failed to close stderr: %v", err)
}
if err := streamOut.Close(); err != nil {
logrus.Debugf("failed to close stream output: %v", err)
}
}()
var err error
if tty {
_, err = io.Copy(stdout, streamOut)
} else {
_, err = stdcopy.StdCopy(stdout, stderr, streamOut)
}
if err != nil && !errors.Is(err, io.EOF) {
logrus.Debugf("stream copy error for container %s: %v", container, err)
}
}()
}
return nil
}
func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.ReadCloser, error) {
cnx, err := s.apiClient().ContainerAttach(ctx, container, client.ContainerAttachOptions{
Stream: true,
Stdin: false,
Stdout: true,
Stderr: true,
Logs: false,
})
if err == nil {
stdout := ContainerStdout{HijackedResponse: cnx.HijackedResponse}
return stdout, nil
}
// Fallback to logs API
logs, err := s.apiClient().ContainerLogs(ctx, container, client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
})
if err != nil {
return nil, err
}
return logs, nil
}
================================================
FILE: pkg/compose/attach_service.go
================================================
/*
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"
"strings"
"github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Attach(ctx context.Context, projectName string, options api.AttachOptions) error {
projectName = strings.ToLower(projectName)
target, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index)
if err != nil {
return err
}
detachKeys := options.DetachKeys
if detachKeys == "" {
detachKeys = s.configFile().DetachKeys
}
var attach container.AttachOptions
attach.DetachKeys = detachKeys
attach.NoStdin = options.NoStdin
attach.Proxy = options.Proxy
return container.RunAttach(ctx, s.dockerCli, target.ID, &attach)
}
================================================
FILE: pkg/compose/build.go
================================================
/*
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"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
err := options.Apply(project)
if err != nil {
return err
}
return Run(ctx, func(ctx context.Context) error {
return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
builtImages, err := s.build(ctx, project, options, nil)
if err == nil && len(builtImages) == 0 {
logrus.Warn("No services to build")
}
return err
})(ctx)
}, "build", s.events)
}
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]api.ImageSummary) (map[string]string, error) {
imageIDs := map[string]string{}
serviceToBuild := types.Services{}
var policy types.DependencyOption = types.IgnoreDependencies
if options.Deps {
policy = types.IncludeDependencies
}
if len(options.Services) == 0 {
options.Services = project.ServiceNames()
}
// also include services used as additional_contexts with service: prefix
options.Services = addBuildDependencies(options.Services, project)
// Some build dependencies we just introduced may not be enabled
var err error
project, err = project.WithServicesEnabled(options.Services...)
if err != nil {
return nil, err
}
project, err = project.WithSelectedServices(options.Services)
if err != nil {
return nil, err
}
err = project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
if service.Build == nil {
return nil
}
image := api.GetImageNameOrDefault(*service, project.Name)
_, localImagePresent := localImages[image]
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
return nil
}
serviceToBuild[serviceName] = *service
return nil
}, policy)
if err != nil {
return imageIDs, err
}
if len(serviceToBuild) == 0 {
return imageIDs, nil
}
bake, err := buildWithBake(s.dockerCli)
if err != nil {
return nil, err
}
if bake {
return s.doBuildBake(ctx, project, serviceToBuild, options)
}
return s.doBuildClassic(ctx, project, serviceToBuild, options)
}
func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {
for name, service := range project.Services {
if service.Provider == nil && service.Image == "" && service.Build == nil {
return fmt.Errorf("invalid service %q. Must specify either image or build", name)
}
}
images, err := s.getLocalImagesDigests(ctx, project)
if err != nil {
return err
}
err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
return s.pullRequiredImages(ctx, project, images, quietPull)
},
)(ctx)
if err != nil {
return err
}
if buildOpts != nil {
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
builtImages, err := s.build(ctx, project, *buildOpts, images)
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = api.ImageSummary{
Repository: name,
ID: digest,
LastTagTime: time.Now(),
}
}
return nil
},
)(ctx)
if err != nil {
return err
}
}
// set digest as com.docker.compose.image label so we can detect outdated containers
for name, service := range project.Services {
image := api.GetImageNameOrDefault(service, project.Name)
img, ok := images[image]
if ok {
service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
}
resolveImageVolumes(&service, images, project.Name)
project.Services[name] = service
}
return nil
}
func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
for i, vol := range service.Volumes {
if vol.Type == types.VolumeTypeImage {
imgName := vol.Source
if _, ok := images[vol.Source]; !ok {
// check if source is another service in the project
imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
if _, ok := images[imgName]; !ok {
continue
}
}
if img, ok := images[imgName]; ok {
// Use Image ID directly as source.
// Using name@digest format (via reference.WithDigest) fails for local-only images
// that don't have RepoDigests (e.g. built locally in CI).
// Image ID (sha256:...) is always valid and ensures ServiceHash changes on rebuild.
service.Volumes[i].Source = img.ID
}
}
}
}
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
imageNames := utils.Set[string]{}
for _, s := range project.Services {
imageNames.Add(api.GetImageNameOrDefault(s, project.Name))
for _, volume := range s.Volumes {
if volume.Type == types.VolumeTypeImage {
imageNames.Add(volume.Source)
}
}
}
imgs, err := s.getImageSummaries(ctx, imageNames.Elements())
if err != nil {
return nil, err
}
for i, service := range project.Services {
imgName := api.GetImageNameOrDefault(service, project.Name)
img, ok := imgs[imgName]
if !ok {
continue
}
if service.Platform != "" {
platform, err := platforms.Parse(service.Platform)
if err != nil {
return nil, err
}
inspect, err := s.apiClient().ImageInspect(ctx, img.ID)
if err != nil {
return nil, err
}
actual := specs.Platform{
Architecture: inspect.Architecture,
OS: inspect.Os,
Variant: inspect.Variant,
}
if !platforms.NewMatcher(platform).Match(actual) {
logrus.Debugf("local image %s doesn't match expected platform %s", service.Image, service.Platform)
// there is a local image, but it's for the wrong platform, so
// pretend it doesn't exist so that we can pull/build an image
// for the correct platform instead
delete(imgs, imgName)
}
}
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, img.ID)
}
return imgs, nil
}
// resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build.
//
// First, args directly defined via `build.args` in YAML are considered.
// Then, any explicitly passed args in opts (e.g. via `--build-arg` on the CLI) are merged, overwriting any
// keys that already exist.
// Next, any keys without a value are resolved using the project environment.
//
// Finally, standard proxy variables based on the Docker client configuration are added, but will not overwrite
// any values if already present.
func resolveAndMergeBuildArgs(proxyConfig map[string]string, project *types.Project, service types.ServiceConfig, opts api.BuildOptions) types.MappingWithEquals {
result := make(types.MappingWithEquals).
OverrideBy(service.Build.Args).
OverrideBy(opts.Args).
Resolve(envResolver(project.Environment))
// proxy arguments do NOT override and should NOT have env resolution applied,
// so they're handled last
for k, v := range proxyConfig {
if _, ok := result[k]; !ok {
v := v
result[k] = &v
}
}
return result
}
func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
ret := make(types.Labels)
if service.Build != nil {
for k, v := range service.Build.Labels {
ret.Add(k, v)
}
}
ret.Add(api.VersionLabel, api.ComposeVersion)
ret.Add(api.ProjectLabel, project.Name)
ret.Add(api.ServiceLabel, service.Name)
return ret
}
func addBuildDependencies(services []string, project *types.Project) []string {
servicesWithDependencies := utils.NewSet(services...)
for _, service := range services {
s, ok := project.Services[service]
if !ok {
s = project.DisabledServices[service]
}
b := s.Build
if b != nil {
for _, target := range b.AdditionalContexts {
if s, found := strings.CutPrefix(target, types.ServicePrefix); found {
servicesWithDependencies.Add(s)
}
}
}
}
if len(servicesWithDependencies) > len(services) {
return addBuildDependencies(servicesWithDependencies.Elements(), project)
}
return servicesWithDependencies.Elements()
}
================================================
FILE: pkg/compose/build_bake.go
================================================
/*
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 (
"bufio"
"bytes"
"context"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/console"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
"github.com/docker/cli/cli/streams"
"github.com/google/uuid"
"github.com/moby/buildkit/client"
gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/moby/moby/client/pkg/versions"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func buildWithBake(dockerCli command.Cli) (bool, error) {
enabled, err := dockerCli.BuildKitEnabled()
if err != nil {
return false, err
}
if !enabled {
return false, nil
}
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
if err != nil {
if errdefs.IsNotFound(err) {
logrus.Warnf("Docker Compose requires buildx plugin to be installed")
return false, nil
}
return false, err
}
return true, err
}
// We _could_ use bake.* types from github.com/docker/buildx but long term plan is to remove buildx as a dependency
type bakeConfig struct {
Groups map[string]bakeGroup `json:"group"`
Targets map[string]bakeTarget `json:"target"`
}
type bakeGroup struct {
Targets []string `json:"targets"`
}
type bakeTarget struct {
Context string `json:"context,omitempty"`
Contexts map[string]string `json:"contexts,omitempty"`
Dockerfile string `json:"dockerfile,omitempty"`
DockerfileInline string `json:"dockerfile-inline,omitempty"`
Args map[string]string `json:"args,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Tags []string `json:"tags,omitempty"`
CacheFrom []string `json:"cache-from,omitempty"`
CacheTo []string `json:"cache-to,omitempty"`
Target string `json:"target,omitempty"`
Secrets []string `json:"secret,omitempty"`
SSH []string `json:"ssh,omitempty"`
Platforms []string `json:"platforms,omitempty"`
Pull bool `json:"pull,omitempty"`
NoCache bool `json:"no-cache,omitempty"`
NetworkMode string `json:"network,omitempty"`
NoCacheFilter []string `json:"no-cache-filter,omitempty"`
ShmSize types.UnitBytes `json:"shm-size,omitempty"`
Ulimits []string `json:"ulimits,omitempty"`
Call string `json:"call,omitempty"`
Entitlements []string `json:"entitlements,omitempty"`
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
Outputs []string `json:"output,omitempty"`
Attest []string `json:"attest,omitempty"`
}
type bakeMetadata map[string]buildStatus
type buildStatus struct {
Digest string `json:"containerimage.digest"`
Image string `json:"image.name"`
}
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
eg := errgroup.Group{}
ch := make(chan *client.SolveStatus)
displayMode := progressui.DisplayMode(options.Progress)
if p, ok := os.LookupEnv("BUILDKIT_PROGRESS"); ok && displayMode == progressui.AutoMode {
displayMode = progressui.DisplayMode(p)
}
out := options.Out
if out == nil {
out = s.stdout()
}
display, err := progressui.NewDisplay(makeConsole(out), displayMode)
if err != nil {
return nil, err
}
eg.Go(func() error {
_, err := display.UpdateFrom(ctx, ch)
return err
})
cfg := bakeConfig{
Groups: map[string]bakeGroup{},
Targets: map[string]bakeTarget{},
}
var (
group bakeGroup
privileged bool
read []string
expectedImages = make(map[string]string, len(serviceToBeBuild)) // service name -> expected image
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
)
// produce a unique ID for service used as bake target
for serviceName := range project.Services {
t := strings.ReplaceAll(serviceName, ".", "_")
for {
if _, ok := targets[serviceName]; !ok {
targets[serviceName] = t
break
}
t += "_"
}
}
var secretsEnv []string
for serviceName, service := range project.Services {
if service.Build == nil {
continue
}
buildConfig := *service.Build
labels := getImageBuildLabels(project, service)
args := resolveAndMergeBuildArgs(s.getProxyConfig(), project, service, options).ToMapping()
for k, v := range args {
args[k] = strings.ReplaceAll(v, "${", "$${")
}
entitlements := buildConfig.Entitlements
if slices.Contains(buildConfig.Entitlements, "security.insecure") {
privileged = true
}
if buildConfig.Privileged {
entitlements = append(entitlements, "security.insecure")
privileged = true
}
var outputs []string
var call string
push := options.Push && service.Image != ""
switch {
case options.Check:
call = "lint"
case len(service.Build.Platforms) > 1:
outputs = []string{fmt.Sprintf("type=image,push=%t", push)}
default:
if push {
outputs = []string{"type=registry"}
} else {
outputs = []string{"type=docker"}
}
}
read = append(read, buildConfig.Context)
for _, path := range buildConfig.AdditionalContexts {
_, _, err := gitutil.ParseGitRef(path)
if !strings.Contains(path, "://") && err != nil {
read = append(read, path)
}
}
image := api.GetImageNameOrDefault(service, project.Name)
s.events.On(buildingEvent(image))
expectedImages[serviceName] = image
pull := service.Build.Pull || options.Pull
noCache := service.Build.NoCache || options.NoCache
target := targets[serviceName]
secrets, env := toBakeSecrets(project, buildConfig.Secrets)
secretsEnv = append(secretsEnv, env...)
cfg.Targets[target] = bakeTarget{
Context: buildConfig.Context,
Contexts: additionalContexts(buildConfig.AdditionalContexts, targets),
Dockerfile: dockerFilePath(buildConfig.Context, buildConfig.Dockerfile),
DockerfileInline: strings.ReplaceAll(buildConfig.DockerfileInline, "${", "$${"),
Args: args,
Labels: labels,
Tags: append(buildConfig.Tags, image),
CacheFrom: buildConfig.CacheFrom,
CacheTo: buildConfig.CacheTo,
NetworkMode: buildConfig.Network,
NoCacheFilter: buildConfig.NoCacheFilter,
Platforms: buildConfig.Platforms,
Target: buildConfig.Target,
Secrets: secrets,
SSH: toBakeSSH(append(buildConfig.SSH, options.SSHs...)),
Pull: pull,
NoCache: noCache,
ShmSize: buildConfig.ShmSize,
Ulimits: toBakeUlimits(buildConfig.Ulimits),
Entitlements: entitlements,
ExtraHosts: toBakeExtraHosts(buildConfig.ExtraHosts),
Outputs: outputs,
Call: call,
Attest: toBakeAttest(buildConfig),
}
}
// create a bake group with targets for services to build
for serviceName, service := range serviceToBeBuild {
if service.Build == nil {
continue
}
group.Targets = append(group.Targets, targets[serviceName])
}
cfg.Groups["default"] = group
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, err
}
if options.Print {
_, err = fmt.Fprintln(s.stdout(), string(b))
return nil, err
}
logrus.Debugf("bake build config:\n%s", string(b))
tmpdir := os.TempDir()
var metadataFile string
for {
// we don't use os.CreateTemp here as we need a temporary file name, but don't want it actually created
// as bake relies on atomicwriter and this creates conflict during rename
metadataFile = filepath.Join(tmpdir, fmt.Sprintf("compose-build-metadataFile-%s.json", uuid.New().String()))
if _, err = os.Stat(metadataFile); err != nil {
if os.IsNotExist(err) {
break
}
var pathError *fs.PathError
if errors.As(err, &pathError) {
return nil, fmt.Errorf("can't access os.tempDir %s: %w", tmpdir, pathError.Err)
}
}
}
defer func() {
_ = os.Remove(metadataFile)
}()
buildx, err := s.getBuildxPlugin()
if err != nil {
return nil, err
}
args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadataFile}
// FIXME we should prompt user about this, but this is a breaking change in UX
for _, path := range read {
args = append(args, "--allow", "fs.read="+path)
}
if privileged {
args = append(args, "--allow", "security.insecure")
}
if options.SBOM != "" {
args = append(args, "--sbom="+options.SBOM)
}
if options.Provenance != "" {
args = append(args, "--provenance="+options.Provenance)
}
if options.Builder != "" {
args = append(args, "--builder", options.Builder)
}
if options.Quiet {
args = append(args, "--progress=quiet")
}
logrus.Debugf("Executing bake with args: %v", args)
if s.dryRun {
return s.dryRunBake(cfg), nil
}
cmd := exec.CommandContext(ctx, buildx.Path, args...)
err = s.prepareShellOut(ctx, types.NewMapping(os.Environ()), cmd)
if err != nil {
return nil, err
}
endpoint, cleanup, err := s.propagateDockerEndpoint()
if err != nil {
return nil, err
}
cmd.Env = append(cmd.Env, endpoint...)
cmd.Env = append(cmd.Env, secretsEnv...)
defer cleanup()
cmd.Stdout = s.stdout()
cmd.Stdin = bytes.NewBuffer(b)
pipe, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
var errMessage []string
reader := bufio.NewReader(pipe)
err = cmd.Start()
if err != nil {
return nil, err
}
eg.Go(cmd.Wait)
for {
line, readErr := reader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break
}
if errors.Is(readErr, os.ErrClosed) {
logrus.Debugf("bake stopped")
break
}
return nil, fmt.Errorf("failed to execute bake: %w", readErr)
}
decoder := json.NewDecoder(strings.NewReader(line))
var status client.SolveStatus
err := decoder.Decode(&status)
if err != nil {
if strings.HasPrefix(line, "ERROR: ") {
errMessage = append(errMessage, line[7:])
} else {
errMessage = append(errMessage, line)
}
continue
}
ch <- &status
}
close(ch) // stop build progress UI
err = eg.Wait()
if err != nil {
if len(errMessage) > 0 {
return nil, errors.New(strings.Join(errMessage, "\n"))
}
return nil, fmt.Errorf("failed to execute bake: %w", err)
}
b, err = os.ReadFile(metadataFile)
if err != nil {
return nil, err
}
var md bakeMetadata
err = json.Unmarshal(b, &md)
if err != nil {
return nil, err
}
results := map[string]string{}
for name := range serviceToBeBuild {
image := expectedImages[name]
target := targets[name]
built, ok := md[target]
if !ok {
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
}
results[image] = built.Digest
s.events.On(builtEvent(image))
}
return results, nil
}
func (s *composeService) getBuildxPlugin() (*manager.Plugin, error) {
buildx, err := manager.GetPlugin("buildx", s.dockerCli, &cobra.Command{})
if err != nil {
return nil, err
}
if buildx.Err != nil {
return nil, buildx.Err
}
if buildx.Version == "" {
return nil, fmt.Errorf("failed to get version of buildx")
}
if versions.LessThan(buildx.Version[1:], buildxMinVersion) {
return nil, fmt.Errorf("compose build requires buildx %s or later", buildxMinVersion)
}
return buildx, nil
}
// makeConsole wraps the provided writer to match [containerd.File] interface if it is of type *streams.Out.
// buildkit's NewDisplay doesn't actually require a [io.Reader], it only uses the [containerd.Console] type to
// benefits from ANSI capabilities, but only does writes.
func makeConsole(out io.Writer) io.Writer {
if s, ok := out.(*streams.Out); ok {
return &_console{s}
}
return out
}
var _ console.File = &_console{}
type _console struct {
*streams.Out
}
func (c _console) Read([]byte) (n int, err error) {
return 0, errors.New("not implemented")
}
func (c _console) Close() error {
return nil
}
func (c _console) Fd() uintptr {
return c.FD()
}
func (c _console) Name() string {
return "compose"
}
func toBakeExtraHosts(hosts types.HostsList) map[string]string {
m := make(map[string]string)
for k, v := range hosts {
m[k] = strings.Join(v, ",")
}
return m
}
func additionalContexts(contexts types.Mapping, targets map[string]string) map[string]string {
ac := map[string]string{}
for k, v := range contexts {
if target, found := strings.CutPrefix(v, types.ServicePrefix); found {
v = "target:" + targets[target]
}
ac[k] = v
}
return ac
}
func toBakeUlimits(ulimits map[string]*types.UlimitsConfig) []string {
s := []string{}
for u, l := range ulimits {
if l.Single > 0 {
s = append(s, fmt.Sprintf("%s=%d", u, l.Single))
} else {
s = append(s, fmt.Sprintf("%s=%d:%d", u, l.Soft, l.Hard))
}
}
return s
}
func toBakeSSH(ssh types.SSHConfig) []string {
var s []string
for _, key := range ssh {
s = append(s, fmt.Sprintf("%s=%s", key.ID, key.Path))
}
return s
}
func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig) ([]string, []string) {
var s []string
var env []string
for _, ref := range secrets {
def := project.Secrets[ref.Source]
target := ref.Target
if target == "" {
target = ref.Source
}
switch {
case def.Environment != "":
env = append(env, fmt.Sprintf("%s=%s", def.Environment, project.Environment[def.Environment]))
s = append(s, fmt.Sprintf("id=%s,type=env,env=%s", target, def.Environment))
case def.File != "":
s = append(s, fmt.Sprintf("id=%s,type=file,src=%s", target, def.File))
}
}
return s, env
}
func toBakeAttest(buildConfig types.BuildConfig) []string {
var attests []string
// Handle per-service provenance configuration (only from build config, not global options)
if buildConfig.Provenance != "" {
if buildConfig.Provenance == "true" {
attests = append(attests, "type=provenance")
} else if buildConfig.Provenance != "false" {
attests = append(attests, fmt.Sprintf("type=provenance,%s", buildConfig.Provenance))
}
}
// Handle per-service SBOM configuration (only from build config, not global options)
if buildConfig.SBOM != "" {
if buildConfig.SBOM == "true" {
attests = append(attests, "type=sbom")
} else if buildConfig.SBOM != "false" {
attests = append(attests, fmt.Sprintf("type=sbom,%s", buildConfig.SBOM))
}
}
return attests
}
func dockerFilePath(ctxName string, dockerfile string) string {
if dockerfile == "" {
return ""
}
if contextType, _ := build.DetectContextType(ctxName); contextType == build.ContextTypeGit {
return dockerfile
}
if !filepath.IsAbs(dockerfile) {
dockerfile = filepath.Join(ctxName, dockerfile)
}
dir := filepath.Dir(dockerfile)
symlinks, err := filepath.EvalSymlinks(dir)
if err == nil {
return filepath.Join(symlinks, filepath.Base(dockerfile))
}
return dockerfile
}
func (s composeService) dryRunBake(cfg bakeConfig) map[string]string {
bakeResponse := map[string]string{}
for name, target := range cfg.Targets {
dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name)))
s.displayDryRunBuildEvent(name, dryRunUUID, target.Tags[0])
bakeResponse[name] = dryRunUUID
}
for name := range bakeResponse {
s.events.On(builtEvent(name))
}
return bakeResponse
}
func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) {
s.events.On(api.Resource{
ID: name + " ==>",
Status: api.Done,
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
})
s.events.On(api.Resource{
ID: name + " ==> ==>",
Status: api.Done,
Text: fmt.Sprintf(`naming to %s`, tag),
})
}
================================================
FILE: pkg/compose/build_classic.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command/image/build"
"github.com/moby/go-archive"
buildtypes "github.com/moby/moby/api/types/build"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
"github.com/moby/moby/client/pkg/progress"
"github.com/moby/moby/client/pkg/streamformatter"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) doBuildClassic(ctx context.Context, project *types.Project, serviceToBuild types.Services, options api.BuildOptions) (map[string]string, error) {
imageIDs := map[string]string{}
// Not using bake, additional_context: service:xx is implemented by building images in dependency order
project, err := project.WithServicesTransform(func(serviceName string, service types.ServiceConfig) (types.ServiceConfig, error) {
if service.Build != nil {
for _, c := range service.Build.AdditionalContexts {
if t, found := strings.CutPrefix(c, types.ServicePrefix); found {
if service.DependsOn == nil {
service.DependsOn = map[string]types.ServiceDependency{}
}
service.DependsOn[t] = types.ServiceDependency{
Condition: "build", // non-canonical, but will force dependency graph ordering
}
}
}
}
return service, nil
})
if err != nil {
return imageIDs, err
}
// we use a pre-allocated []string to collect build digest by service index while running concurrent goroutines
builtDigests := make([]string, len(project.Services))
names := project.ServiceNames()
getServiceIndex := func(name string) int {
for idx, n := range names {
if n == name {
return idx
}
}
return -1
}
err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
trace.SpanFromContext(ctx).SetAttributes(attribute.String("builder", "classic"))
service, ok := serviceToBuild[name]
if !ok {
return nil
}
image := api.GetImageNameOrDefault(service, project.Name)
s.events.On(buildingEvent(image))
id, err := s.doBuildImage(ctx, project, service, options)
if err != nil {
return err
}
s.events.On(builtEvent(image))
builtDigests[getServiceIndex(name)] = id
if options.Push {
return s.push(ctx, project, api.PushOptions{})
}
return nil
}, func(traversal *graphTraversal) {
traversal.maxConcurrency = s.maxConcurrency
})
if err != nil {
return nil, err
}
for i, imageDigest := range builtDigests {
if imageDigest != "" {
service := project.Services[names[i]]
imageRef := api.GetImageNameOrDefault(service, project.Name)
imageIDs[imageRef] = imageDigest
}
}
return imageIDs, err
}
//nolint:gocyclo
func (s *composeService) doBuildImage(ctx context.Context, project *types.Project, service types.ServiceConfig, options api.BuildOptions) (string, error) {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
contextDir string
relDockerfile string
)
if len(service.Build.Platforms) > 1 {
return "", fmt.Errorf("the classic builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use BuildKit")
}
if service.Build.Privileged {
return "", fmt.Errorf("the classic builder doesn't support privileged mode, set DOCKER_BUILDKIT=1 to use BuildKit")
}
if len(service.Build.AdditionalContexts) > 0 {
return "", fmt.Errorf("the classic builder doesn't support additional contexts, set DOCKER_BUILDKIT=1 to use BuildKit")
}
if len(service.Build.SSH) > 0 {
return "", fmt.Errorf("the classic builder doesn't support SSH keys, set DOCKER_BUILDKIT=1 to use BuildKit")
}
if len(service.Build.Secrets) > 0 {
return "", fmt.Errorf("the classic builder doesn't support secrets, set DOCKER_BUILDKIT=1 to use BuildKit")
}
if service.Build.Labels == nil {
service.Build.Labels = make(map[string]string)
}
service.Build.Labels[api.ImageBuilderLabel] = "classic"
dockerfileName := dockerFilePath(service.Build.Context, service.Build.Dockerfile)
specifiedContext := service.Build.Context
progBuff := s.stdout()
buildBuff := s.stdout()
contextType, err := build.DetectContextType(specifiedContext)
if err != nil {
return "", err
}
switch contextType {
case build.ContextTypeStdin:
return "", fmt.Errorf("building from STDIN is not supported")
case build.ContextTypeLocal:
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName)
if err != nil {
return "", fmt.Errorf("unable to prepare context: %w", err)
}
if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
// Dockerfile is outside build-context; read the Dockerfile and pass it as dockerfileCtx
dockerfileCtx, err = os.Open(dockerfileName)
if err != nil {
return "", fmt.Errorf("unable to open Dockerfile: %w", err)
}
defer dockerfileCtx.Close() //nolint:errcheck
}
case build.ContextTypeGit:
var tempDir string
tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, dockerfileName)
if err != nil {
return "", fmt.Errorf("unable to prepare context: %w", err)
}
defer func() {
_ = os.RemoveAll(tempDir)
}()
contextDir = tempDir
case build.ContextTypeRemote:
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, dockerfileName)
if err != nil {
return "", fmt.Errorf("unable to prepare context: %w", err)
}
default:
return "", fmt.Errorf("unable to prepare context: path %q not found", specifiedContext)
}
// read from a directory into tar archive
if buildCtx == nil {
excludes, err := build.ReadDockerignore(contextDir)
if err != nil {
return "", err
}
if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
return "", fmt.Errorf("checking context: %w", err)
}
// And canonicalize dockerfile name to a platform-independent one
relDockerfile = filepath.ToSlash(relDockerfile)
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, false)
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &archive.ChownOpts{UID: 0, GID: 0},
})
if err != nil {
return "", err
}
}
// replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context
if dockerfileCtx != nil && buildCtx != nil {
buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
if err != nil {
return "", err
}
}
buildCtx, err = build.Compress(buildCtx)
if err != nil {
return "", err
}
// Setup an upload progress bar
progressOutput := streamformatter.NewProgressOutput(progBuff)
body := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
configFile := s.configFile()
creds, err := configFile.GetAllCredentials()
if err != nil {
return "", err
}
authConfigs := make(map[string]registry.AuthConfig, len(creds))
for k, authConfig := range creds {
authConfigs[k] = registry.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
ServerAddress: authConfig.ServerAddress,
// TODO(thaJeztah): Are these expected to be included? See https://github.com/docker/cli/pull/6516#discussion_r2387586472
Auth: authConfig.Auth,
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
}
}
buildOpts := imageBuildOptions(s.getProxyConfig(), project, service, options)
imageName := api.GetImageNameOrDefault(service, project.Name)
buildOpts.Tags = append(buildOpts.Tags, imageName)
buildOpts.Dockerfile = relDockerfile
buildOpts.AuthConfigs = authConfigs
buildOpts.Memory = options.Memory
ctx, cancel := context.WithCancel(ctx)
defer cancel()
s.events.On(buildingEvent(imageName))
response, err := s.apiClient().ImageBuild(ctx, body, buildOpts)
if err != nil {
return "", err
}
defer response.Body.Close() //nolint:errcheck
imageID := ""
aux := func(msg jsonstream.Message) {
var result buildtypes.Result
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
logrus.Errorf("Failed to parse aux message: %s", err)
} else {
imageID = result.ID
}
}
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.FD(), true, aux)
if err != nil {
var jerr *jsonstream.Error
if errors.As(err, &jerr) {
// If no error code is set, default to 1
if jerr.Code == 0 {
jerr.Code = 1
}
return "", cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
}
return "", err
}
s.events.On(builtEvent(imageName))
return imageID, nil
}
func imageBuildOptions(proxyConfigs map[string]string, project *types.Project, service types.ServiceConfig, options api.BuildOptions) client.ImageBuildOptions {
config := service.Build
return client.ImageBuildOptions{
Version: buildtypes.BuilderV1,
Tags: config.Tags,
NoCache: config.NoCache,
Remove: true,
PullParent: config.Pull,
BuildArgs: resolveAndMergeBuildArgs(proxyConfigs, project, service, options),
Labels: config.Labels,
NetworkMode: config.Network,
ExtraHosts: config.ExtraHosts.AsList(":"),
Target: config.Target,
Isolation: container.Isolation(config.Isolation),
}
}
================================================
FILE: pkg/compose/build_test.go
================================================
/*
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 (
"slices"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func Test_addBuildDependencies(t *testing.T) {
project := &types.Project{Services: types.Services{
"test": types.ServiceConfig{
Build: &types.BuildConfig{
AdditionalContexts: map[string]string{
"foo": "service:foo",
"bar": "service:bar",
},
},
},
"foo": types.ServiceConfig{
Build: &types.BuildConfig{
AdditionalContexts: map[string]string{
"zot": "service:zot",
},
},
},
"bar": types.ServiceConfig{
Build: &types.BuildConfig{},
},
"zot": types.ServiceConfig{
Build: &types.BuildConfig{},
},
}}
services := addBuildDependencies([]string{"test"}, project)
expected := []string{"test", "foo", "bar", "zot"}
slices.Sort(services)
slices.Sort(expected)
assert.DeepEqual(t, services, expected)
}
================================================
FILE: pkg/compose/commit.go
================================================
/*
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"
"strings"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.commit(ctx, projectName, options)
}, "commit", s.events)
}
func (s *composeService) commit(ctx context.Context, projectName string, options api.CommitOptions) error {
projectName = strings.ToLower(projectName)
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index)
if err != nil {
return err
}
name := getCanonicalContainerName(ctr)
s.events.On(api.Resource{
ID: name,
Status: api.Working,
Text: api.StatusCommitting,
})
if s.dryRun {
s.events.On(api.Resource{
ID: name,
Status: api.Done,
Text: api.StatusCommitted,
})
return nil
}
response, err := s.apiClient().ContainerCommit(ctx, ctr.ID, client.ContainerCommitOptions{
Reference: options.Reference,
Comment: options.Comment,
Author: options.Author,
Changes: options.Changes.GetSlice(),
NoPause: !options.Pause,
})
if err != nil {
return err
}
s.events.On(api.Resource{
ID: name,
Text: fmt.Sprintf("Committed as %s", response.ID),
Status: api.Done,
})
return nil
}
================================================
FILE: pkg/compose/compose.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"io"
"strconv"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams"
"github.com/jonboulle/clockwork"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/dryrun"
)
type Option func(service *composeService) error
// NewComposeService creates a Compose service using Docker CLI.
// This is the standard constructor that requires command.Cli for full functionality.
//
// Example usage:
//
// dockerCli, _ := command.NewDockerCli()
// service := NewComposeService(dockerCli)
//
// For advanced configuration with custom overrides, use ServiceOption functions:
//
// service := NewComposeService(dockerCli,
// WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
// WithOutputStream(customOut),
// WithErrorStream(customErr),
// WithInputStream(customIn))
//
// Or set all streams at once:
//
// service := NewComposeService(dockerCli,
// WithStreams(customOut, customErr, customIn))
func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, error) {
s := &composeService{
dockerCli: dockerCli,
clock: clockwork.NewRealClock(),
maxConcurrency: -1,
dryRun: false,
}
for _, option := range options {
if err := option(s); err != nil {
return nil, err
}
}
if s.prompt == nil {
s.prompt = func(message string, defaultValue bool) (bool, error) {
fmt.Println(message)
logrus.Warning("Compose is running without a 'prompt' component to interact with user")
return defaultValue, nil
}
}
if s.events == nil {
s.events = &ignore{}
}
// If custom streams were provided, wrap the Docker CLI to use them
if s.outStream != nil || s.errStream != nil || s.inStream != nil {
s.dockerCli = s.wrapDockerCliWithStreams(dockerCli)
}
return s, nil
}
// WithStreams sets custom I/O streams for output and interaction
func WithStreams(out, err io.Writer, in io.Reader) Option {
return func(s *composeService) error {
s.outStream = out
s.errStream = err
s.inStream = in
return nil
}
}
// WithOutputStream sets a custom output stream
func WithOutputStream(out io.Writer) Option {
return func(s *composeService) error {
s.outStream = out
return nil
}
}
// WithErrorStream sets a custom error stream
func WithErrorStream(err io.Writer) Option {
return func(s *composeService) error {
s.errStream = err
return nil
}
}
// WithInputStream sets a custom input stream
func WithInputStream(in io.Reader) Option {
return func(s *composeService) error {
s.inStream = in
return nil
}
}
// WithContextInfo sets custom Docker context information
func WithContextInfo(info api.ContextInfo) Option {
return func(s *composeService) error {
s.contextInfo = info
return nil
}
}
// WithProxyConfig sets custom HTTP proxy configuration for builds
func WithProxyConfig(config map[string]string) Option {
return func(s *composeService) error {
s.proxyConfig = config
return nil
}
}
// WithPrompt configure a UI component for Compose service to interact with user and confirm actions
func WithPrompt(prompt Prompt) Option {
return func(s *composeService) error {
s.prompt = prompt
return nil
}
}
// WithMaxConcurrency defines upper limit for concurrent operations against engine API
func WithMaxConcurrency(maxConcurrency int) Option {
return func(s *composeService) error {
s.maxConcurrency = maxConcurrency
return nil
}
}
// WithDryRun configure Compose to run without actually applying changes
func WithDryRun(s *composeService) error {
s.dryRun = true
cli, err := command.NewDockerCli()
if err != nil {
return err
}
options := flags.NewClientOptions()
options.Context = s.dockerCli.CurrentContext()
err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
return dryrun.NewDryRunClient(s.apiClient(), s.dockerCli)
}))
if err != nil {
return err
}
s.dockerCli = cli
return nil
}
type Prompt func(message string, defaultValue bool) (bool, error)
// AlwaysOkPrompt returns a Prompt implementation that always returns true without user interaction.
func AlwaysOkPrompt() Prompt {
return func(message string, defaultValue bool) (bool, error) {
return true, nil
}
}
// WithEventProcessor configure component to get notified on Compose operation and progress events.
// Typically used to configure a progress UI
func WithEventProcessor(bus api.EventProcessor) Option {
return func(s *composeService) error {
s.events = bus
return nil
}
}
type composeService struct {
dockerCli command.Cli
// prompt is used to interact with user and confirm actions
prompt Prompt
// eventBus collects tasks execution events
events api.EventProcessor
// Optional overrides for specific components (for SDK users)
outStream io.Writer
errStream io.Writer
inStream io.Reader
contextInfo api.ContextInfo
proxyConfig map[string]string
clock clockwork.Clock
maxConcurrency int
dryRun bool
}
// Close releases any connections/resources held by the underlying clients.
//
// In practice, this service has the same lifetime as the process, so everything
// will get cleaned up at about the same time regardless even if not invoked.
func (s *composeService) Close() error {
var errs []error
if s.dockerCli != nil {
errs = append(errs, s.apiClient().Close())
}
return errors.Join(errs...)
}
func (s *composeService) apiClient() client.APIClient {
return s.dockerCli.Client()
}
func (s *composeService) configFile() *configfile.ConfigFile {
return s.dockerCli.ConfigFile()
}
// getContextInfo returns the context info - either custom override or dockerCli adapter
func (s *composeService) getContextInfo() api.ContextInfo {
if s.contextInfo != nil {
return s.contextInfo
}
return &dockerCliContextInfo{cli: s.dockerCli}
}
// getProxyConfig returns the proxy config - either custom override or environment-based
func (s *composeService) getProxyConfig() map[string]string {
if s.proxyConfig != nil {
return s.proxyConfig
}
return storeutil.GetProxyConfig(s.dockerCli)
}
func (s *composeService) stdout() *streams.Out {
return s.dockerCli.Out()
}
func (s *composeService) stdin() *streams.In {
return s.dockerCli.In()
}
func (s *composeService) stderr() *streams.Out {
return s.dockerCli.Err()
}
// readCloserAdapter adapts io.Reader to io.ReadCloser
type readCloserAdapter struct {
r io.Reader
}
func (r *readCloserAdapter) Read(p []byte) (int, error) {
return r.r.Read(p)
}
func (r *readCloserAdapter) Close() error {
return nil
}
// wrapDockerCliWithStreams wraps the Docker CLI to intercept and override stream methods
func (s *composeService) wrapDockerCliWithStreams(baseCli command.Cli) command.Cli {
wrapper := &streamOverrideWrapper{
Cli: baseCli,
}
// Wrap custom streams in Docker CLI's stream types
if s.outStream != nil {
wrapper.outStream = streams.NewOut(s.outStream)
}
if s.errStream != nil {
wrapper.errStream = streams.NewOut(s.errStream)
}
if s.inStream != nil {
wrapper.inStream = streams.NewIn(&readCloserAdapter{r: s.inStream})
}
return wrapper
}
// streamOverrideWrapper wraps command.Cli to override streams with custom implementations
type streamOverrideWrapper struct {
command.Cli
outStream *streams.Out
errStream *streams.Out
inStream *streams.In
}
func (w *streamOverrideWrapper) Out() *streams.Out {
if w.outStream != nil {
return w.outStream
}
return w.Cli.Out()
}
func (w *streamOverrideWrapper) Err() *streams.Out {
if w.errStream != nil {
return w.errStream
}
return w.Cli.Err()
}
func (w *streamOverrideWrapper) In() *streams.In {
if w.inStream != nil {
return w.inStream
}
return w.Cli.In()
}
func getCanonicalContainerName(c container.Summary) string {
if len(c.Names) == 0 {
// corner case, sometime happens on removal. return short ID as a safeguard value
return c.ID[:12]
}
// Names return container canonical name /foo + link aliases /linked_by/foo
for _, name := range c.Names {
if strings.LastIndex(name, "/") == 0 {
return name[1:]
}
}
return strings.TrimPrefix(c.Names[0], "/")
}
func getContainerNameWithoutProject(c container.Summary) string {
project := c.Labels[api.ProjectLabel]
defaultName := getDefaultContainerName(project, c.Labels[api.ServiceLabel], c.Labels[api.ContainerNumberLabel])
name := getCanonicalContainerName(c)
if name != defaultName {
// service declares a custom container_name
return name
}
return name[len(project)+1:]
}
// projectFromName builds a types.Project based on actual resources with compose labels set
func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
project := &types.Project{
Name: projectName,
Services: types.Services{},
}
if len(containers) == 0 {
return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
}
set := types.Services{}
for _, c := range containers {
serviceLabel, ok := c.Labels[api.ServiceLabel]
if !ok {
serviceLabel = getCanonicalContainerName(c)
}
service, ok := set[serviceLabel]
if !ok {
service = types.ServiceConfig{
Name: serviceLabel,
Image: c.Image,
Labels: c.Labels,
}
}
service.Scale = increment(service.Scale)
set[serviceLabel] = service
}
for name, service := range set {
dependencies := service.Labels[api.DependenciesLabel]
if dependencies != "" {
service.DependsOn = types.DependsOnConfig{}
for dc := range strings.SplitSeq(dependencies, ",") {
dcArr := strings.Split(dc, ":")
condition := ServiceConditionRunningOrHealthy
// Let's restart the dependency by default if we don't have the info stored in the label
restart := true
required := true
dependency := dcArr[0]
// backward compatibility
if len(dcArr) > 1 {
condition = dcArr[1]
if len(dcArr) > 2 {
restart, _ = strconv.ParseBool(dcArr[2])
}
}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
}
set[name] = service
}
}
project.Services = set
SERVICES:
for _, qs := range services {
for _, es := range project.Services {
if es.Name == qs {
continue SERVICES
}
}
return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
}
project, err := project.WithSelectedServices(services)
if err != nil {
return project, err
}
return project, nil
}
func increment(scale *int) *int {
i := 1
if scale != nil {
i = *scale + 1
}
return &i
}
func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
opts := client.VolumeListOptions{
Filters: projectFilter(projectName),
}
volumes, err := s.apiClient().VolumeList(ctx, opts)
if err != nil {
return nil, err
}
actual := types.Volumes{}
for _, vol := range volumes.Items {
actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
Name: vol.Name,
Driver: vol.Driver,
Labels: vol.Labels,
}
}
return actual, nil
}
func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
networks, err := s.apiClient().NetworkList(ctx, client.NetworkListOptions{
Filters: projectFilter(projectName),
})
if err != nil {
return nil, err
}
actual := types.Networks{}
for _, net := range networks.Items {
actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
Name: net.Name,
Driver: net.Driver,
Labels: net.Labels,
}
}
return actual, nil
}
var swarmEnabled = struct {
once sync.Once
val bool
err error
}{}
func (s *composeService) isSwarmEnabled(ctx context.Context) (bool, error) {
swarmEnabled.once.Do(func() {
res, err := s.apiClient().Info(ctx, client.InfoOptions{})
if err != nil {
swarmEnabled.err = err
}
switch res.Info.Swarm.LocalNodeState {
case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
swarmEnabled.val = false
default:
swarmEnabled.val = true
}
})
return swarmEnabled.val, swarmEnabled.err
}
type runtimeVersionCache struct {
once sync.Once
val string
err error
}
var runtimeVersion runtimeVersionCache
func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
// TODO(thaJeztah): this should use Client.ClientVersion), which has the negotiated version.
runtimeVersion.once.Do(func() {
version, err := s.apiClient().ServerVersion(ctx, client.ServerVersionOptions{})
if err != nil {
runtimeVersion.err = err
}
runtimeVersion.val = version.APIVersion
})
return runtimeVersion.val, runtimeVersion.err
}
================================================
FILE: pkg/compose/container.go
================================================
/*
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 (
"io"
"github.com/moby/moby/client"
)
var _ io.ReadCloser = ContainerStdout{}
// ContainerStdout implement ReadCloser for moby.HijackedResponse
type ContainerStdout struct {
client.HijackedResponse
}
// Read implement io.ReadCloser
func (l ContainerStdout) Read(p []byte) (n int, err error) {
return l.Reader.Read(p)
}
// Close implement io.ReadCloser
func (l ContainerStdout) Close() error {
l.HijackedResponse.Close()
return nil
}
var _ io.WriteCloser = ContainerStdin{}
// ContainerStdin implement WriteCloser for moby.HijackedResponse
type ContainerStdin struct {
client.HijackedResponse
}
// Write implement io.WriteCloser
func (c ContainerStdin) Write(p []byte) (n int, err error) {
return c.Conn.Write(p)
}
// Close implement io.WriteCloser
func (c ContainerStdin) Close() error {
return c.CloseWrite()
}
================================================
FILE: pkg/compose/containers.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"sort"
"strconv"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
// Containers is a set of moby Container
type Containers []container.Summary
type oneOff int
const (
oneOffInclude = oneOff(iota)
oneOffExclude
oneOffOnly
)
func (s *composeService) getContainers(ctx context.Context, project string, oneOff oneOff, all bool, selectedServices ...string) (Containers, error) {
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: getDefaultFilters(project, oneOff, selectedServices...),
All: all,
})
if err != nil {
return nil, err
}
containers := Containers(res.Items)
if len(selectedServices) > 1 {
containers = containers.filter(isService(selectedServices...))
}
return containers, nil
}
func getDefaultFilters(projectName string, oneOff oneOff, selectedServices ...string) client.Filters {
f := projectFilter(projectName)
if len(selectedServices) == 1 {
f.Add("label", serviceFilter(selectedServices[0]))
}
f.Add("label", hasConfigHashLabel())
switch oneOff {
case oneOffOnly:
f.Add("label", oneOffFilter(true))
case oneOffExclude:
f.Add("label", oneOffFilter(false))
case oneOffInclude:
}
return f
}
func (s *composeService) getSpecifiedContainer(ctx context.Context, projectName string, oneOff oneOff, all bool, serviceName string, containerIndex int) (container.Summary, error) {
defaultFilters := getDefaultFilters(projectName, oneOff, serviceName)
if containerIndex > 0 {
defaultFilters.Add("label", containerNumberFilter(containerIndex))
}
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: defaultFilters,
All: all,
})
if err != nil {
return container.Summary{}, err
}
containers := res.Items
if len(containers) < 1 {
if containerIndex > 0 {
return container.Summary{}, fmt.Errorf("service %q is not running container #%d", serviceName, containerIndex)
}
return container.Summary{}, fmt.Errorf("service %q is not running", serviceName)
}
// Sort by container number first, then put one-off containers at the end
sort.Slice(containers, func(i, j int) bool {
numberLabelX, _ := strconv.Atoi(containers[i].Labels[api.ContainerNumberLabel])
numberLabelY, _ := strconv.Atoi(containers[j].Labels[api.ContainerNumberLabel])
IsOneOffLabelTrueX := containers[i].Labels[api.OneoffLabel] == "True"
IsOneOffLabelTrueY := containers[j].Labels[api.OneoffLabel] == "True"
if IsOneOffLabelTrueX || IsOneOffLabelTrueY {
return !IsOneOffLabelTrueX && IsOneOffLabelTrueY
}
return numberLabelX < numberLabelY
})
return containers[0], nil
}
// containerPredicate define a predicate we want container to satisfy for filtering operations
type containerPredicate func(c container.Summary) bool
func matches(c container.Summary, predicates ...containerPredicate) bool {
for _, predicate := range predicates {
if !predicate(c) {
return false
}
}
return true
}
func isService(services ...string) containerPredicate {
return func(c container.Summary) bool {
service := c.Labels[api.ServiceLabel]
return slices.Contains(services, service)
}
}
// isOrphaned is a predicate to select containers without a matching service definition in compose project
func isOrphaned(project *types.Project) containerPredicate {
services := append(project.ServiceNames(), project.DisabledServiceNames()...)
return func(c container.Summary) bool {
// One-off container
v, ok := c.Labels[api.OneoffLabel]
if ok && v == "True" {
return c.State == container.StateExited || c.State == container.StateDead
}
// Service that is not defined in the compose model
service := c.Labels[api.ServiceLabel]
return !slices.Contains(services, service)
}
}
func isNotOneOff(c container.Summary) bool {
v, ok := c.Labels[api.OneoffLabel]
return !ok || v == "False"
}
// filter return Containers with elements to match predicate
func (containers Containers) filter(predicates ...containerPredicate) Containers {
var filtered Containers
for _, c := range containers {
if matches(c, predicates...) {
filtered = append(filtered, c)
}
}
return filtered
}
func (containers Containers) names() []string {
var names []string
for _, c := range containers {
names = append(names, getCanonicalContainerName(c))
}
return names
}
func (containers Containers) forEach(fn func(container.Summary)) {
for _, c := range containers {
fn(c)
}
}
func (containers Containers) sorted() Containers {
sort.Slice(containers, func(i, j int) bool {
return getCanonicalContainerName(containers[i]) < getCanonicalContainerName(containers[j])
})
return containers
}
================================================
FILE: pkg/compose/convergence.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/moby/moby/api/types/container"
mmount "github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
const (
doubledContainerNameWarning = "WARNING: The %q service is using the custom container name %q. " +
"Docker requires each container to have a unique name. " +
"Remove the custom name to scale the service"
)
// convergence manages service's container lifecycle.
// Based on initially observed state, it reconciles the existing container with desired state, which might include
// re-creating container, adding or removing replicas, or starting stopped containers.
// Cross services dependencies are managed by creating services in expected order and updating `service:xx` reference
// when a service has converged, so dependent ones can be managed with resolved containers references.
type convergence struct {
compose *composeService
services map[string]Containers
networks map[string]string
volumes map[string]string
stateMutex sync.Mutex
}
func (c *convergence) getObservedState(serviceName string) Containers {
c.stateMutex.Lock()
defer c.stateMutex.Unlock()
return c.services[serviceName]
}
func (c *convergence) setObservedState(serviceName string, containers Containers) {
c.stateMutex.Lock()
defer c.stateMutex.Unlock()
c.services[serviceName] = containers
}
func newConvergence(services []string, state Containers, networks map[string]string, volumes map[string]string, s *composeService) *convergence {
observedState := map[string]Containers{}
for _, s := range services {
observedState[s] = Containers{}
}
for _, c := range state.filter(isNotOneOff) {
service := c.Labels[api.ServiceLabel]
observedState[service] = append(observedState[service], c)
}
return &convergence{
compose: s,
services: observedState,
networks: networks,
volumes: volumes,
}
}
func (c *convergence) apply(ctx context.Context, project *types.Project, options api.CreateOptions) error {
return InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
service, err := project.GetService(name)
if err != nil {
return err
}
return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error {
strategy := options.RecreateDependencies
if slices.Contains(options.Services, name) {
strategy = options.Recreate
}
return c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
})(ctx)
})
}
func (c *convergence) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error { //nolint:gocyclo
if service.Provider != nil {
return c.compose.runPlugin(ctx, project, service, "up")
}
expected, err := getScale(service)
if err != nil {
return err
}
containers := c.getObservedState(service.Name)
actual := len(containers)
updated := make(Containers, expected)
eg, ctx := errgroup.WithContext(ctx)
err = c.resolveServiceReferences(&service)
if err != nil {
return err
}
sort.Slice(containers, func(i, j int) bool {
// select obsolete containers first, so they get removed as we scale down
if obsolete, _ := c.mustRecreate(service, containers[i], recreate); obsolete {
// i is obsolete, so must be first in the list
return true
}
if obsolete, _ := c.mustRecreate(service, containers[j], recreate); obsolete {
// j is obsolete, so must be first in the list
return false
}
// For up-to-date containers, sort by container number to preserve low-values in container numbers
ni, erri := strconv.Atoi(containers[i].Labels[api.ContainerNumberLabel])
nj, errj := strconv.Atoi(containers[j].Labels[api.ContainerNumberLabel])
if erri == nil && errj == nil {
return ni > nj
}
// If we don't get a container number (?) just sort by creation date
return containers[i].Created < containers[j].Created
})
slices.Reverse(containers)
for i, ctr := range containers {
if i >= expected {
// Scale Down
// As we sorted containers, obsolete ones and/or highest number will be removed
ctr := ctr
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(ctr)...)
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "service/scale/down", traceOpts, func(ctx context.Context) error {
return c.compose.stopAndRemoveContainer(ctx, ctr, &service, timeout, false)
}))
continue
}
mustRecreate, err := c.mustRecreate(service, ctr, recreate)
if err != nil {
return err
}
if mustRecreate {
err := c.stopDependentContainers(ctx, project, service)
if err != nil {
return err
}
i, ctr := i, ctr
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "container/recreate", tracing.ContainerOptions(ctr), func(ctx context.Context) error {
recreated, err := c.compose.recreateContainer(ctx, project, service, ctr, inherit, timeout)
updated[i] = recreated
return err
}))
continue
}
// Enforce non-diverged containers are running
name := getContainerProgressName(ctr)
switch ctr.State {
case container.StateRunning:
c.compose.events.On(runningEvent(name))
case container.StateCreated:
case container.StateRestarting:
case container.StateExited:
default:
ctr := ctr
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(ctr), func(ctx context.Context) error {
return c.compose.startContainer(ctx, ctr)
}))
}
updated[i] = ctr
}
next := nextContainerNumber(containers)
for i := 0; i < expected-actual; i++ {
// Scale UP
number := next + i
name := getContainerName(project.Name, service, number)
eventOpts := tracing.SpanOptions{trace.WithAttributes(attribute.String("container.name", name))}
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/scale/up", eventOpts, func(ctx context.Context) error {
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
ctr, err := c.compose.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = ctr
return err
}))
continue
}
err = eg.Wait()
c.setObservedState(service.Name, updated)
return err
}
func (c *convergence) stopDependentContainers(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
// Stop dependent containers, so they will be restarted after service is re-created
dependents := project.GetDependentsForService(service, func(dependency types.ServiceDependency) bool {
return dependency.Restart
})
if len(dependents) == 0 {
return nil
}
err := c.compose.stop(ctx, project.Name, api.StopOptions{
Services: dependents,
Project: project,
}, nil)
if err != nil {
return err
}
for _, name := range dependents {
dependentStates := c.getObservedState(name)
for i, dependent := range dependentStates {
dependent.State = container.StateExited
dependentStates[i] = dependent
}
c.setObservedState(name, dependentStates)
}
return nil
}
func getScale(config types.ServiceConfig) (int, error) {
scale := config.GetScale()
if scale > 1 && config.ContainerName != "" {
return 0, fmt.Errorf(doubledContainerNameWarning,
config.Name,
config.ContainerName)
}
return scale, nil
}
// resolveServiceReferences replaces reference to another service with reference to an actual container
func (c *convergence) resolveServiceReferences(service *types.ServiceConfig) error {
err := c.resolveVolumeFrom(service)
if err != nil {
return err
}
err = c.resolveSharedNamespaces(service)
if err != nil {
return err
}
return nil
}
func (c *convergence) resolveVolumeFrom(service *types.ServiceConfig) error {
for i, vol := range service.VolumesFrom {
spec := strings.Split(vol, ":")
if len(spec) == 0 {
continue
}
if spec[0] == "container" {
service.VolumesFrom[i] = spec[1]
continue
}
name := spec[0]
dependencies := c.getObservedState(name)
if len(dependencies) == 0 {
return fmt.Errorf("cannot share volume with service %s: container missing", name)
}
service.VolumesFrom[i] = dependencies.sorted()[0].ID
}
return nil
}
func (c *convergence) resolveSharedNamespaces(service *types.ServiceConfig) error {
str := service.NetworkMode
if name := getDependentServiceFromMode(str); name != "" {
dependencies := c.getObservedState(name)
if len(dependencies) == 0 {
return fmt.Errorf("cannot share network namespace with service %s: container missing", name)
}
service.NetworkMode = types.ContainerPrefix + dependencies.sorted()[0].ID
}
str = service.Ipc
if name := getDependentServiceFromMode(str); name != "" {
dependencies := c.getObservedState(name)
if len(dependencies) == 0 {
return fmt.Errorf("cannot share IPC namespace with service %s: container missing", name)
}
service.Ipc = types.ContainerPrefix + dependencies.sorted()[0].ID
}
str = service.Pid
if name := getDependentServiceFromMode(str); name != "" {
dependencies := c.getObservedState(name)
if len(dependencies) == 0 {
return fmt.Errorf("cannot share PID namespace with service %s: container missing", name)
}
service.Pid = types.ContainerPrefix + dependencies.sorted()[0].ID
}
return nil
}
func (c *convergence) mustRecreate(expected types.ServiceConfig, actual container.Summary, policy string) (bool, error) {
if policy == api.RecreateNever {
return false, nil
}
if policy == api.RecreateForce {
return true, nil
}
configHash, err := ServiceHash(expected)
if err != nil {
return false, err
}
configChanged := actual.Labels[api.ConfigHashLabel] != configHash
imageUpdated := actual.Labels[api.ImageDigestLabel] != expected.CustomLabels[api.ImageDigestLabel]
if configChanged || imageUpdated {
return true, nil
}
if c.networks != nil && actual.State == "running" {
if checkExpectedNetworks(expected, actual, c.networks) {
return true, nil
}
}
if c.volumes != nil {
if checkExpectedVolumes(expected, actual, c.volumes) {
return true, nil
}
}
return false, nil
}
func checkExpectedNetworks(expected types.ServiceConfig, actual container.Summary, networks map[string]string) bool {
// check the networks container is connected to are the expected ones
for net := range expected.Networks {
id := networks[net]
if id == "swarm" {
// corner-case : swarm overlay network isn't visible until a container is attached
continue
}
found := false
for _, settings := range actual.NetworkSettings.Networks {
if settings.NetworkID == id {
found = true
break
}
}
if !found {
// config is up-to-date but container is not connected to network
return true
}
}
return false
}
func checkExpectedVolumes(expected types.ServiceConfig, actual container.Summary, volumes map[string]string) bool {
// check container's volume mounts and search for the expected ones
for _, vol := range expected.Volumes {
if vol.Type != string(mmount.TypeVolume) {
continue
}
if vol.Source == "" {
continue
}
id := volumes[vol.Source]
found := false
for _, mount := range actual.Mounts {
if mount.Type != mmount.TypeVolume {
continue
}
if mount.Name == id {
found = true
break
}
}
if !found {
// config is up-to-date but container doesn't have volume mounted
return true
}
}
return false
}
func getContainerName(projectName string, service types.ServiceConfig, number int) string {
name := getDefaultContainerName(projectName, service.Name, strconv.Itoa(number))
if service.ContainerName != "" {
name = service.ContainerName
}
return name
}
func getDefaultContainerName(projectName, serviceName, index string) string {
return strings.Join([]string{projectName, serviceName, index}, api.Separator)
}
func getContainerProgressName(ctr container.Summary) string {
return "Container " + getCanonicalContainerName(ctr)
}
func containerEvents(containers Containers, eventFunc func(string) api.Resource) []api.Resource {
events := []api.Resource{}
for _, ctr := range containers {
events = append(events, eventFunc(getContainerProgressName(ctr)))
}
return events
}
func containerReasonEvents(containers Containers, eventFunc func(string, string) api.Resource, reason string) []api.Resource {
events := []api.Resource{}
for _, ctr := range containers {
events = append(events, eventFunc(getContainerProgressName(ctr), reason))
}
return events
}
// ServiceConditionRunningOrHealthy is a service condition on status running or healthy
const ServiceConditionRunningOrHealthy = "running_or_healthy"
//nolint:gocyclo
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, ctx := errgroup.WithContext(ctx)
for dep, config := range dependencies {
if shouldWait, err := shouldWaitForDependency(dep, config, project); err != nil {
return err
} else if !shouldWait {
continue
}
waitingFor := containers.filter(isService(dep), isNotOneOff)
s.events.On(containerEvents(waitingFor, waiting)...)
if len(waitingFor) == 0 {
if config.Required {
return fmt.Errorf("%s is missing dependency %s", dependant, dep)
}
logrus.Warnf("%s is missing dependency %s", dependant, dep)
continue
}
eg.Go(func() error {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
case <-ctx.Done():
return nil
}
switch config.Condition {
case ServiceConditionRunningOrHealthy:
isHealthy, err := s.isServiceHealthy(ctx, waitingFor, true)
if err != nil {
if !config.Required {
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %q is not running or is unhealthy", dep))...)
logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error())
return nil
}
return err
}
if isHealthy {
s.events.On(containerEvents(waitingFor, healthy)...)
return nil
}
case types.ServiceConditionHealthy:
isHealthy, err := s.isServiceHealthy(ctx, waitingFor, false)
if err != nil {
if !config.Required {
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %q failed to start", dep))...)
logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error())
return nil
}
s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
return errorEventf(s, "dependency %s failed to start", dep)
})...)
return fmt.Errorf("dependency failed to start: %w", err)
}
if isHealthy {
s.events.On(containerEvents(waitingFor, healthy)...)
return nil
}
case types.ServiceConditionCompletedSuccessfully:
isExited, code, err := s.isServiceCompleted(ctx, waitingFor)
if err != nil {
return err
}
if isExited {
if code == 0 {
s.events.On(containerEvents(waitingFor, exited)...)
return nil
}
messageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code)
if !config.Required {
// optional -> mark as skipped & don't propagate error
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %s", messageSuffix))...)
logrus.Warnf("optional dependency %s", messageSuffix)
return nil
}
msg := fmt.Sprintf("service %s", messageSuffix)
s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
return errorEventf(s, "service %s", messageSuffix)
})...)
return errors.New(msg)
}
default:
logrus.Warnf("unsupported depends_on condition: %s", config.Condition)
return nil
}
}
})
}
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) {
if dependencyConfig.Condition == types.ServiceConditionStarted {
// already managed by InDependencyOrder
return false, nil
}
if service, err := project.GetService(serviceName); err != nil {
for _, ds := range project.DisabledServices {
if ds.Name == serviceName {
// don't wait for disabled service (--no-deps)
return false, nil
}
}
return false, err
} else if service.GetScale() == 0 {
// don't wait for the dependency which configured to have 0 containers running
return false, nil
} else if service.Provider != nil {
// don't wait for provider services
return false, nil
}
return true, nil
}
func nextContainerNumber(containers []container.Summary) int {
maxNumber := 0
for _, c := range containers {
s, ok := c.Labels[api.ContainerNumberLabel]
if !ok {
logrus.Warnf("container %s is missing %s label", c.ID, api.ContainerNumberLabel)
}
n, err := strconv.Atoi(s)
if err != nil {
logrus.Warnf("container %s has invalid %s label: %s", c.ID, api.ContainerNumberLabel, s)
continue
}
if n > maxNumber {
maxNumber = n
}
}
return maxNumber + 1
}
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
name string, number int, opts createOptions,
) (ctr container.Summary, err error) {
eventName := "Container " + name
s.events.On(creatingEvent(eventName))
ctr, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts)
if err != nil {
if ctx.Err() == nil {
s.events.On(api.Resource{
ID: eventName,
Status: api.Error,
Text: err.Error(),
})
}
return ctr, err
}
s.events.On(createdEvent(eventName))
return ctr, nil
}
func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
replaced container.Summary, inherit bool, timeout *time.Duration,
) (created container.Summary, err error) {
eventName := getContainerProgressName(replaced)
s.events.On(newEvent(eventName, api.Working, "Recreate"))
defer func() {
if err != nil && ctx.Err() == nil {
s.events.On(api.Resource{
ID: eventName,
Status: api.Error,
Text: err.Error(),
})
}
}()
number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])
if err != nil {
return created, err
}
var inherited *container.Summary
if inherit {
inherited = &replaced
}
replacedContainerName := service.ContainerName
if replacedContainerName == "" {
replacedContainerName = service.Name + api.Separator + strconv.Itoa(number)
}
name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replacedContainerName),
}
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts)
if err != nil {
return created, err
}
timeoutInSecond := utils.DurationSecondToInt(timeout)
_, err = s.apiClient().ContainerStop(ctx, replaced.ID, client.ContainerStopOptions{Timeout: timeoutInSecond})
if err != nil {
return created, err
}
_, err = s.apiClient().ContainerRemove(ctx, replaced.ID, client.ContainerRemoveOptions{})
if err != nil {
return created, err
}
_, err = s.apiClient().ContainerRename(ctx, tmpName, client.ContainerRenameOptions{
NewName: name,
})
if err != nil {
return created, err
}
s.events.On(newEvent(eventName, api.Done, "Recreated"))
return created, err
}
// force sequential calls to ContainerStart to prevent race condition in engine assigning ports from ranges
var startMx sync.Mutex
func (s *composeService) startContainer(ctx context.Context, ctr container.Summary) error {
s.events.On(newEvent(getContainerProgressName(ctr), api.Working, "Restart"))
startMx.Lock()
defer startMx.Unlock()
_, err := s.apiClient().ContainerStart(ctx, ctr.ID, client.ContainerStartOptions{})
if err != nil {
return err
}
s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Restarted"))
return nil
}
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
name string, number int, inherit *container.Summary, opts createOptions,
) (container.Summary, error) {
var created container.Summary
cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
if err != nil {
return created, err
}
platform := service.Platform
if platform == "" {
platform = project.Environment["DOCKER_DEFAULT_PLATFORM"]
}
var plat *specs.Platform
if platform != "" {
var p specs.Platform
p, err = platforms.Parse(platform)
if err != nil {
return created, err
}
plat = &p
}
response, err := s.apiClient().ContainerCreate(ctx, client.ContainerCreateOptions{
Name: name,
Platform: plat,
Config: cfgs.Container,
HostConfig: cfgs.Host,
NetworkingConfig: cfgs.Network,
})
if err != nil {
return created, err
}
for _, warning := range response.Warnings {
s.events.On(api.Resource{
ID: service.Name,
Status: api.Warning,
Text: warning,
})
}
res, err := s.apiClient().ContainerInspect(ctx, response.ID, client.ContainerInspectOptions{})
if err != nil {
return created, err
}
created = container.Summary{
ID: res.Container.ID,
Labels: res.Container.Config.Labels,
Names: []string{res.Container.Name},
NetworkSettings: &container.NetworkSettingsSummary{
Networks: res.Container.NetworkSettings.Networks,
},
}
return created, nil
}
// getLinks mimics V1 compose/service.py::Service::_get_links()
func (s *composeService) getLinks(ctx context.Context, projectName string, service types.ServiceConfig, number int) ([]string, error) {
var links []string
format := func(k, v string) string {
return fmt.Sprintf("%s:%s", k, v)
}
getServiceContainers := func(serviceName string) (Containers, error) {
return s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
}
for _, rawLink := range service.Links {
// linkName if informed like in: "serviceName[:linkName]"
linkServiceName, linkName, ok := strings.Cut(rawLink, ":")
if !ok {
linkName = linkServiceName
}
cnts, err := getServiceContainers(linkServiceName)
if err != nil {
return nil, err
}
for _, c := range cnts {
containerName := getCanonicalContainerName(c)
links = append(links,
format(containerName, linkName),
format(containerName, linkServiceName+api.Separator+strconv.Itoa(number)),
format(containerName, strings.Join([]string{projectName, linkServiceName, strconv.Itoa(number)}, api.Separator)),
)
}
}
if service.Labels[api.OneoffLabel] == "True" {
cnts, err := getServiceContainers(service.Name)
if err != nil {
return nil, err
}
for _, c := range cnts {
containerName := getCanonicalContainerName(c)
links = append(links,
format(containerName, service.Name),
format(containerName, strings.TrimPrefix(containerName, projectName+api.Separator)),
format(containerName, containerName),
)
}
}
for _, rawExtLink := range service.ExternalLinks {
externalLink, linkName, ok := strings.Cut(rawExtLink, ":")
if !ok {
linkName = externalLink
}
links = append(links, format(externalLink, linkName))
}
return links, nil
}
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
for _, c := range containers {
res, err := s.apiClient().ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err != nil {
return false, err
}
ctr := res.Container
name := ctr.Name[1:]
if ctr.State.Status == container.StateExited {
return false, fmt.Errorf("container %s exited (%d)", name, ctr.State.ExitCode)
}
noHealthcheck := ctr.Config.Healthcheck == nil || (len(ctr.Config.Healthcheck.Test) > 0 && ctr.Config.Healthcheck.Test[0] == "NONE")
if noHealthcheck && fallbackRunning {
// Container does not define a health check, but we can fall back to "running" state
return ctr.State != nil && ctr.State.Status == container.StateRunning, nil
}
if ctr.State == nil || ctr.State.Health == nil {
return false, fmt.Errorf("container %s has no healthcheck configured", name)
}
switch ctr.State.Health.Status {
case container.Healthy:
// Continue by checking the next container.
case container.Unhealthy:
return false, fmt.Errorf("container %s is unhealthy", name)
case container.Starting:
return false, nil
default:
return false, fmt.Errorf("container %s had unexpected health status %q", name, ctr.State.Health.Status)
}
}
return true, nil
}
func (s *composeService) isServiceCompleted(ctx context.Context, containers Containers) (bool, int, error) {
for _, c := range containers {
res, err := s.apiClient().ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err != nil {
return false, 0, err
}
if res.Container.State != nil && res.Container.State.Status == container.StateExited {
return true, res.Container.State.ExitCode, nil
}
}
return false, 0, nil
}
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, timeout)
if err != nil {
return err
}
if len(containers) == 0 {
if service.GetScale() == 0 {
return nil
}
return fmt.Errorf("service %q has no container to start", service.Name)
}
for _, ctr := range containers.filter(isService(service.Name)) {
if ctr.State == container.StateRunning {
continue
}
err = s.injectSecrets(ctx, project, service, ctr.ID)
if err != nil {
return err
}
err = s.injectConfigs(ctx, project, service, ctr.ID)
if err != nil {
return err
}
eventName := getContainerProgressName(ctr)
s.events.On(startingEvent(eventName))
_, err = s.apiClient().ContainerStart(ctx, ctr.ID, client.ContainerStartOptions{})
if err != nil {
return err
}
for _, hook := range service.PostStart {
err = s.runHook(ctx, ctr, service, hook, listener)
if err != nil {
return err
}
}
s.events.On(startedEvent(eventName))
}
return nil
}
func mergeLabels(ls ...types.Labels) types.Labels {
merged := types.Labels{}
for _, l := range ls {
maps.Copy(merged, l)
}
return merged
}
================================================
FILE: pkg/compose/convergence_test.go
================================================
/*
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"
"net/netip"
"strings"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestContainerName(t *testing.T) {
s := types.ServiceConfig{
Name: "testservicename",
ContainerName: "testcontainername",
Scale: intPtr(1),
Deploy: &types.DeployConfig{},
}
ret, err := getScale(s)
assert.NilError(t, err)
assert.Equal(t, ret, *s.Scale)
s.Scale = intPtr(0)
ret, err = getScale(s)
assert.NilError(t, err)
assert.Equal(t, ret, *s.Scale)
s.Scale = intPtr(2)
_, err = getScale(s)
assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName))
}
func intPtr(i int) *int {
return &i
}
func TestServiceLinks(t *testing.T) {
const dbContainerName = "/" + testProject + "-db-1"
const webContainerName = "/" + testProject + "-web-1"
s := types.ServiceConfig{
Name: "web",
Scale: intPtr(1),
}
containerListOptions := client.ContainerListOptions{
Filters: projectFilter(testProject).Add("label",
serviceFilter("db"),
oneOffFilter(false),
hasConfigHashLabel(),
),
All: true,
}
t.Run("service links default", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db"}
c := testContainer("db", dbContainerName, false)
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
Items: []container.Summary{c},
}, nil)
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
assert.NilError(t, err)
assert.Equal(t, len(links), 3)
assert.Equal(t, links[0], "testProject-db-1:db")
assert.Equal(t, links[1], "testProject-db-1:db-1")
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
})
t.Run("service links", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:db"}
c := testContainer("db", dbContainerName, false)
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
Items: []container.Summary{c},
}, nil)
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
assert.NilError(t, err)
assert.Equal(t, len(links), 3)
assert.Equal(t, links[0], "testProject-db-1:db")
assert.Equal(t, links[1], "testProject-db-1:db-1")
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
})
t.Run("service links name", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:dbname"}
c := testContainer("db", dbContainerName, false)
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
Items: []container.Summary{c},
}, nil)
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
assert.NilError(t, err)
assert.Equal(t, len(links), 3)
assert.Equal(t, links[0], "testProject-db-1:dbname")
assert.Equal(t, links[1], "testProject-db-1:db-1")
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
})
t.Run("service links external links", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:dbname"}
s.ExternalLinks = []string{"db1:db2"}
c := testContainer("db", dbContainerName, false)
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
Items: []container.Summary{c},
}, nil)
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
assert.NilError(t, err)
assert.Equal(t, len(links), 4)
assert.Equal(t, links[0], "testProject-db-1:dbname")
assert.Equal(t, links[1], "testProject-db-1:db-1")
assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
// ExternalLink
assert.Equal(t, links[3], "db1:db2")
})
t.Run("service links itself oneoff", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{}
s.ExternalLinks = []string{}
s.Labels = s.Labels.Add(api.OneoffLabel, "True")
c := testContainer("web", webContainerName, true)
containerListOptionsOneOff := client.ContainerListOptions{
Filters: projectFilter(testProject).Add("label",
serviceFilter("web"),
oneOffFilter(false),
hasConfigHashLabel(),
),
All: true,
}
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptionsOneOff).Return(client.ContainerListResult{
Items: []container.Summary{c},
}, nil)
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
assert.NilError(t, err)
assert.Equal(t, len(links), 3)
assert.Equal(t, links[0], "testProject-web-1:web")
assert.Equal(t, links[1], "testProject-web-1:web-1")
assert.Equal(t, links[2], "testProject-web-1:testProject-web-1")
})
}
func TestWaitDependencies(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
t.Run("should skip dependencies with scale 0", func(t *testing.T) {
dbService := types.ServiceConfig{Name: "db", Scale: intPtr(0)}
redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(0)}
project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
"db": dbService,
"redis": redisService,
}}
dependencies := types.DependsOnConfig{
"db": {Condition: ServiceConditionRunningOrHealthy},
"redis": {Condition: ServiceConditionRunningOrHealthy},
}
assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &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)}
redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(1)}
project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
"db": dbService,
"redis": redisService,
}}
dependencies := types.DependsOnConfig{
"db": {Condition: types.ServiceConditionStarted, Required: true},
"redis": {Condition: types.ServiceConditionStarted, Required: true},
}
assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &project, "", dependencies, nil, 0))
})
}
func TestIsServiceHealthy(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
ctx := t.Context()
t.Run("disabled healthcheck with fallback to running", func(t *testing.T) {
containerID := "test-container-id"
containers := Containers{
{ID: containerID},
}
// Container with disabled healthcheck (Test: ["NONE"])
apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: containerID,
Name: "test-container",
State: &container.State{Status: "running"},
Config: &container.Config{
Healthcheck: &container.HealthConfig{
Test: []string{"NONE"},
},
},
},
}, nil)
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
assert.NilError(t, err)
assert.Equal(t, true, isHealthy, "Container with disabled healthcheck should be considered healthy when running with fallbackRunning=true")
})
t.Run("disabled healthcheck without fallback", func(t *testing.T) {
containerID := "test-container-id"
containers := Containers{
{ID: containerID},
}
// Container with disabled healthcheck (Test: ["NONE"]) but fallbackRunning=false
apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: containerID,
Name: "test-container",
State: &container.State{Status: "running"},
Config: &container.Config{
Healthcheck: &container.HealthConfig{
Test: []string{"NONE"},
},
},
},
}, nil)
_, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
assert.ErrorContains(t, err, "has no healthcheck configured")
})
t.Run("no healthcheck with fallback to running", func(t *testing.T) {
containerID := "test-container-id"
containers := Containers{
{ID: containerID},
}
// Container with no healthcheck at all
apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: containerID,
Name: "test-container",
State: &container.State{Status: "running"},
Config: &container.Config{
Healthcheck: nil,
},
},
}, nil)
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
assert.NilError(t, err)
assert.Equal(t, true, isHealthy, "Container with no healthcheck should be considered healthy when running with fallbackRunning=true")
})
t.Run("exited container with disabled healthcheck", func(t *testing.T) {
containerID := "test-container-id"
containers := Containers{
{ID: containerID},
}
// Container with disabled healthcheck but exited
apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: containerID,
Name: "test-container",
State: &container.State{
Status: "exited",
ExitCode: 1,
},
Config: &container.Config{
Healthcheck: &container.HealthConfig{
Test: []string{"NONE"},
},
},
},
}, nil)
_, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
assert.ErrorContains(t, err, "exited")
})
t.Run("healthy container with healthcheck", func(t *testing.T) {
containerID := "test-container-id"
containers := Containers{
{ID: containerID},
}
// Container with actual healthcheck that is healthy
apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: containerID,
Name: "test-container",
State: &container.State{
Status: "running",
Health: &container.Health{
Status: container.Healthy,
},
},
Config: &container.Config{
Healthcheck: &container.HealthConfig{
Test: []string{"CMD", "curl", "-f", "http://localhost"},
},
},
},
}, nil)
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
assert.NilError(t, err)
assert.Equal(t, true, isHealthy, "Container with healthy status should be healthy")
})
}
func TestCreateMobyContainer(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).Return(client.ImageInspectResult{}, nil).AnyTimes()
// force `RuntimeVersion` to fetch fresh version
runtimeVersion = runtimeVersionCache{}
apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{
APIVersion: "1.44",
}, nil).AnyTimes()
service := types.ServiceConfig{
Name: "test",
Networks: map[string]*types.ServiceNetworkConfig{
"a": {
Priority: 10,
},
"b": {
Priority: 100,
},
},
}
project := types.Project{
Name: "bork",
Services: types.Services{
"test": service,
},
Networks: types.Networks{
"a": types.NetworkConfig{
Name: "a-moby-name",
},
"b": types.NetworkConfig{
Name: "b-moby-name",
},
},
}
var got client.ContainerCreateOptions
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
got = opts
return client.ContainerCreateResult{ID: "an-id"}, nil
})
apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id"), gomock.Any()).Times(1).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: "an-id",
Name: "a-name",
Config: &container.Config{},
NetworkSettings: &container.NetworkSettings{},
},
}, nil)
_, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
Labels: make(types.Labels),
})
var falseBool bool
want := client.ContainerCreateOptions{
Config: &container.Config{
AttachStdout: true,
AttachStderr: true,
Image: "bork-test",
Labels: map[string]string{
"com.docker.compose.config-hash": "8dbce408396f8986266bc5deba0c09cfebac63c95c2238e405c7bee5f1bd84b8",
"com.docker.compose.depends_on": "",
},
},
HostConfig: &container.HostConfig{
PortBindings: network.PortMap{},
ExtraHosts: []string{},
Tmpfs: map[string]string{},
Resources: container.Resources{
OomKillDisable: &falseBool,
},
NetworkMode: "b-moby-name",
},
NetworkingConfig: &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
"a-moby-name": {
IPAMConfig: &network.EndpointIPAMConfig{},
Aliases: []string{"bork-test-0"},
},
"b-moby-name": {
IPAMConfig: &network.EndpointIPAMConfig{},
Aliases: []string{"bork-test-0"},
},
},
},
Name: "test",
}
assert.DeepEqual(t, want, got, cmpopts.EquateComparable(netip.Addr{}), cmpopts.EquateEmpty())
assert.NilError(t, err)
}
================================================
FILE: pkg/compose/convert.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"time"
compose "github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/api/types/container"
)
// ToMobyEnv convert into []string
func ToMobyEnv(environment compose.MappingWithEquals) []string {
var env []string
for k, v := range environment {
if v == nil {
env = append(env, k)
} else {
env = append(env, fmt.Sprintf("%s=%s", k, *v))
}
}
return env
}
// ToMobyHealthCheck convert into container.HealthConfig
func (s *composeService) ToMobyHealthCheck(ctx context.Context, check *compose.HealthCheckConfig) (*container.HealthConfig, error) {
if check == nil {
return nil, nil
}
var (
interval time.Duration
timeout time.Duration
period time.Duration
retries int
)
if check.Interval != nil {
interval = time.Duration(*check.Interval)
}
if check.Timeout != nil {
timeout = time.Duration(*check.Timeout)
}
if check.StartPeriod != nil {
period = time.Duration(*check.StartPeriod)
}
if check.Retries != nil {
retries = int(*check.Retries)
}
test := check.Test
if check.Disable {
test = []string{"NONE"}
}
var startInterval time.Duration
if check.StartInterval != nil {
startInterval = time.Duration(*check.StartInterval)
if check.StartPeriod == nil {
// see https://github.com/moby/moby/issues/48874
return nil, errors.New("healthcheck.start_interval requires healthcheck.start_period to be set")
}
}
return &container.HealthConfig{
Test: test,
Interval: interval,
Timeout: timeout,
StartPeriod: period,
StartInterval: startInterval,
Retries: retries,
}, nil
}
// ToSeconds convert into seconds
func ToSeconds(d *compose.Duration) *int {
if d == nil {
return nil
}
s := int(time.Duration(*d).Seconds())
return &s
}
================================================
FILE: pkg/compose/cp.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/docker/cli/cli/command"
"github.com/moby/go-archive"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
type copyDirection int
const (
fromService copyDirection = 1 << iota
toService
acrossServices = fromService | toService
)
func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.copy(ctx, projectName, options)
}, "copy", s.events)
}
func (s *composeService) copy(ctx context.Context, projectName string, options api.CopyOptions) error {
projectName = strings.ToLower(projectName)
srcService, srcPath := splitCpArg(options.Source)
destService, dstPath := splitCpArg(options.Destination)
var direction copyDirection
var serviceName string
var copyFunc func(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error
if srcService != "" {
direction |= fromService
serviceName = srcService
copyFunc = s.copyFromContainer
}
if destService != "" {
direction |= toService
serviceName = destService
copyFunc = s.copyToContainer
}
if direction == acrossServices {
return errors.New("copying between services is not supported")
}
if direction == 0 {
return errors.New("unknown copy direction")
}
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options, direction, serviceName)
if err != nil {
return err
}
g := errgroup.Group{}
for _, cont := range containers {
ctr := cont
g.Go(func() error {
name := getCanonicalContainerName(ctr)
var msg string
if direction == fromService {
msg = fmt.Sprintf("%s:%s to %s", name, srcPath, dstPath)
} else {
msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath)
}
s.events.On(api.Resource{
ID: name,
Text: api.StatusCopying,
Details: msg,
Status: api.Working,
})
if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil {
return err
}
s.events.On(api.Resource{
ID: name,
Text: api.StatusCopied,
Details: msg,
Status: api.Done,
})
return nil
})
}
return g.Wait()
}
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 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:
withOneOff := oneOffExclude
if options.All {
withOneOff = oneOffInclude
}
containers, err = s.getContainers(ctx, projectName, withOneOff, true, serviceName)
if err != nil {
return nil, err
}
if len(containers) < 1 {
return nil, fmt.Errorf("no container found for service %q", serviceName)
}
if direction == fromService {
return containers[:1], err
}
return containers, err
}
}
func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {
var err error
if srcPath != "-" {
// Get an absolute source path.
srcPath, err = resolveLocalPath(srcPath)
if err != nil {
return err
}
}
// Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath}
var dstStat container.PathStat
res, err := s.apiClient().ContainerStatPath(ctx, containerID, client.ContainerStatPathOptions{
Path: dstPath,
})
if err == nil {
dstStat = res.Stat
}
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget
if !isAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
}
dstInfo.Path = linkTarget
res, err = s.apiClient().ContainerStatPath(ctx, containerID, client.ContainerStatPathOptions{
Path: linkTarget,
})
if err == nil {
dstStat = res.Stat
}
}
// Validate the destination path
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, containerID, dstPath, err)
}
// Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory
// or vice versa) the extraction will fail. If the destination simply did
// not exist, but the parent directory does, the extraction will still
// succeed.
if err == nil {
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
}
var (
content io.Reader
resolvedDstPath string
)
if srcPath == "-" {
content = s.stdin()
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir {
return fmt.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
}
} else {
// Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)
if err != nil {
return err
}
srcArchive, err := archive.TarResource(srcInfo)
if err != nil {
return err
}
defer srcArchive.Close() //nolint:errcheck
// With the stat info about the local source as well as the
// destination, we have enough information to know whether we need to
// alter the archive that we upload so that when the server extracts
// it to the specified directory in the container we get the desired
// copy behavior.
// See comments in the implementation of `archive.PrepareArchiveCopy`
// for exactly what goes into deciding how and whether the source
// archive needs to be altered for the correct copy behavior when it is
// extracted. This function also infers from the source and destination
// info which directory to extract to, which may be the parent of the
// destination that the user specified.
// Don't create the archive if running in Dry Run mode
if !s.dryRun {
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
if err != nil {
return err
}
defer preparedArchive.Close() //nolint:errcheck
resolvedDstPath = dstDir
content = preparedArchive
}
}
_, err = s.apiClient().CopyToContainer(ctx, containerID, client.CopyToContainerOptions{
DestinationPath: resolvedDstPath,
Content: content,
AllowOverwriteDirWithFile: false,
CopyUIDGID: opts.CopyUIDGID,
})
return err
}
func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
var err error
if dstPath != "-" {
// Get an absolute destination path.
dstPath, err = resolveLocalPath(dstPath)
if err != nil {
return err
}
}
if err := command.ValidateOutputPath(dstPath); err != nil {
return err
}
// if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string
if opts.FollowLink {
var srcStat container.PathStat
res, err := s.apiClient().ContainerStatPath(ctx, containerID, client.ContainerStatPathOptions{
Path: srcPath,
})
if err == nil {
srcStat = res.Stat
}
// If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
linkTarget := srcStat.LinkTarget
if !isAbs(linkTarget) {
// Join with the parent directory.
srcParent, _ := archive.SplitPathDirEntry(srcPath)
linkTarget = filepath.Join(srcParent, linkTarget)
}
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
srcPath = linkTarget
}
}
res, err := s.apiClient().CopyFromContainer(ctx, containerID, client.CopyFromContainerOptions{
SourcePath: srcPath,
})
if err != nil {
return err
}
defer res.Content.Close() //nolint:errcheck
if dstPath == "-" {
_, err = io.Copy(s.stdout(), res.Content)
return err
}
srcInfo := archive.CopyInfo{
Path: srcPath,
Exists: true,
IsDir: res.Stat.Mode.IsDir(),
RebaseName: rebaseName,
}
preArchive := res.Content
if srcInfo.RebaseName != "" {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(res.Content, srcBase, srcInfo.RebaseName)
}
return archive.CopyTo(preArchive, srcInfo, dstPath)
}
// IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
//
// On Windows, golang filepath.IsAbs does not consider a path \windows\system32
// as absolute as it doesn't start with a drive-letter/colon combination. However,
// in docker we need to verify things such as WORKDIR /windows/system32 in
// a Dockerfile (which gets translated to \windows\system32 when being processed
// by the daemon). This SHOULD be treated as absolute from a docker processing
// perspective.
func isAbs(path string) bool {
return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
}
func splitCpArg(arg string) (ctr, path string) {
if isAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return "", arg
}
ctr, path, ok := strings.Cut(arg, ":")
if !ok || strings.HasPrefix(ctr, ".") {
// Either there's no `:` in the arg
// OR it's an explicit local relative path like `./file:name.txt`.
return "", arg
}
return ctr, path
}
func resolveLocalPath(localPath string) (absPath string, err error) {
if absPath, err = filepath.Abs(localPath); err != nil {
return absPath, err
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
}
================================================
FILE: pkg/compose/create.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/moby/moby/api/types/blkiodev"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/versions"
"github.com/sirupsen/logrus"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
"github.com/docker/compose/v5/pkg/api"
)
type createOptions struct {
AutoRemove bool
AttachStdin bool
UseNetworkAliases bool
Labels types.Labels
}
type createConfigs struct {
Container *container.Config
Host *container.HostConfig
Network *network.NetworkingConfig
Links []string
}
func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.create(ctx, project, createOpts)
}, "create", s.events)
}
func (s *composeService) create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
if len(options.Services) == 0 {
options.Services = project.ServiceNames()
}
err := project.CheckContainerNameUnicity()
if err != nil {
return err
}
err = s.ensureImagesExists(ctx, project, options.Build, options.QuietPull)
if err != nil {
return err
}
err = s.ensureModels(ctx, project, options.QuietPull)
if err != nil {
return err
}
prepareNetworks(project)
networks, err := s.ensureNetworks(ctx, project)
if err != nil {
return err
}
volumes, err := s.ensureProjectVolumes(ctx, project)
if err != nil {
return err
}
var observedState Containers
observedState, err = s.getContainers(ctx, project.Name, oneOffInclude, true)
if err != nil {
return err
}
orphans := observedState.filter(isOrphaned(project))
if len(orphans) > 0 && !options.IgnoreOrphans {
if options.RemoveOrphans {
err := s.removeContainers(ctx, orphans, nil, nil, false)
if err != nil {
return err
}
} else {
logrus.Warnf("Found orphan containers (%s) for this project. If "+
"you removed or renamed this service in your compose "+
"file, you can run this command with the "+
"--remove-orphans flag to clean it up.", orphans.names())
}
}
// Temporary implementation of use_api_socket until we get actual support inside docker engine
project, err = s.useAPISocket(project)
if err != nil {
return err
}
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
}
func prepareNetworks(project *types.Project) {
for k, nw := range project.Networks {
nw.CustomLabels = nw.CustomLabels.
Add(api.NetworkLabel, k).
Add(api.ProjectLabel, project.Name).
Add(api.VersionLabel, api.ComposeVersion)
project.Networks[k] = nw
}
}
func (s *composeService) ensureNetworks(ctx context.Context, project *types.Project) (map[string]string, error) {
networks := map[string]string{}
for name, nw := range project.Networks {
id, err := s.ensureNetwork(ctx, project, name, &nw)
if err != nil {
return nil, err
}
networks[name] = id
project.Networks[name] = nw
}
return networks, nil
}
func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) (map[string]string, error) {
ids := map[string]string{}
for k, volume := range project.Volumes {
volume.CustomLabels = volume.CustomLabels.Add(api.VolumeLabel, k)
volume.CustomLabels = volume.CustomLabels.Add(api.ProjectLabel, project.Name)
volume.CustomLabels = volume.CustomLabels.Add(api.VersionLabel, api.ComposeVersion)
id, err := s.ensureVolume(ctx, k, volume, project)
if err != nil {
return nil, err
}
ids[k] = id
}
return ids, nil
}
//nolint:gocyclo
func (s *composeService) getCreateConfigs(ctx context.Context,
p *types.Project,
service types.ServiceConfig,
number int,
inherit *container.Summary,
opts createOptions,
) (createConfigs, error) {
labels, err := s.prepareLabels(opts.Labels, service, number)
if err != nil {
return createConfigs{}, err
}
var runCmd, entrypoint []string
if service.Command != nil {
runCmd = service.Command
}
if service.Entrypoint != nil {
entrypoint = service.Entrypoint
}
var (
tty = service.Tty
stdinOpen = service.StdinOpen
)
proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
env := proxyConfig.OverrideBy(service.Environment)
var mainNwName string
var mainNw *types.ServiceNetworkConfig
if len(service.Networks) > 0 {
mainNwName = service.NetworksByPriority()[0]
mainNw = service.Networks[mainNwName]
}
if err := s.prepareContainerMACAddress(service, mainNw, mainNwName); err != nil {
return createConfigs{}, err
}
healthcheck, err := s.ToMobyHealthCheck(ctx, service.HealthCheck)
if err != nil {
return createConfigs{}, err
}
exposedPorts, err := buildContainerPorts(service)
if err != nil {
return createConfigs{}, err
}
containerConfig := container.Config{
Hostname: service.Hostname,
Domainname: service.DomainName,
User: service.User,
ExposedPorts: exposedPorts,
Tty: tty,
OpenStdin: stdinOpen,
StdinOnce: opts.AttachStdin && stdinOpen,
AttachStdin: opts.AttachStdin,
AttachStderr: true,
AttachStdout: true,
Cmd: runCmd,
Image: api.GetImageNameOrDefault(service, p.Name),
WorkingDir: service.WorkingDir,
Entrypoint: entrypoint,
NetworkDisabled: service.NetworkMode == "disabled",
Labels: labels,
StopSignal: service.StopSignal,
Env: ToMobyEnv(env),
Healthcheck: healthcheck,
StopTimeout: ToSeconds(service.StopGracePeriod),
} // VOLUMES/MOUNTS/FILESYSTEMS
tmpfs := map[string]string{}
for _, t := range service.Tmpfs {
k, v, _ := strings.Cut(t, ":")
tmpfs[k] = v
}
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return createConfigs{}, err
}
// NETWORKING
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return createConfigs{}, err
}
apiVersion, err := s.RuntimeVersion(ctx)
if err != nil {
return createConfigs{}, err
}
networkMode, networkingConfig, err := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
if err != nil {
return createConfigs{}, err
}
portBindings, err := buildContainerPortBindingOptions(service)
if err != nil {
return createConfigs{}, err
}
// MISC
resources := getDeployResources(service)
var logConfig container.LogConfig
if service.Logging != nil {
logConfig = container.LogConfig{
Type: service.Logging.Driver,
Config: service.Logging.Options,
}
}
securityOpts, unconfined, err := parseSecurityOpts(p, service.SecurityOpt)
if err != nil {
return createConfigs{}, err
}
var dnsIPs []netip.Addr
for _, d := range service.DNS {
dnsIP, err := netip.ParseAddr(d)
if err != nil {
return createConfigs{}, fmt.Errorf("invalid DNS address: %w", err)
}
dnsIPs = append(dnsIPs, dnsIP)
}
hostConfig := container.HostConfig{
AutoRemove: opts.AutoRemove,
Annotations: service.Annotations,
Binds: binds,
Mounts: mounts,
CapAdd: service.CapAdd,
CapDrop: service.CapDrop,
NetworkMode: networkMode,
Init: service.Init,
IpcMode: container.IpcMode(service.Ipc),
CgroupnsMode: container.CgroupnsMode(service.Cgroup),
ReadonlyRootfs: service.ReadOnly,
RestartPolicy: getRestartPolicy(service),
ShmSize: int64(service.ShmSize),
Sysctls: service.Sysctls,
PortBindings: portBindings,
Resources: resources,
VolumeDriver: service.VolumeDriver,
VolumesFrom: service.VolumesFrom,
DNS: dnsIPs,
DNSSearch: service.DNSSearch,
DNSOptions: service.DNSOpts,
ExtraHosts: service.ExtraHosts.AsList(":"),
SecurityOpt: securityOpts,
StorageOpt: service.StorageOpt,
UsernsMode: container.UsernsMode(service.UserNSMode),
UTSMode: container.UTSMode(service.Uts),
Privileged: service.Privileged,
PidMode: container.PidMode(service.Pid),
Tmpfs: tmpfs,
Isolation: container.Isolation(service.Isolation),
Runtime: service.Runtime,
LogConfig: logConfig,
GroupAdd: service.GroupAdd,
Links: links,
OomScoreAdj: int(service.OomScoreAdj),
}
if unconfined {
hostConfig.MaskedPaths = []string{}
hostConfig.ReadonlyPaths = []string{}
}
cfgs := createConfigs{
Container: &containerConfig,
Host: &hostConfig,
Network: networkingConfig,
Links: links,
}
return cfgs, nil
}
// prepareContainerMACAddress handles the service-level mac_address field and the newer mac_address field added to service
// network config. This newer field is only compatible with the Engine API v1.44 (and onwards), and this API version
// also deprecates the container-wide mac_address field. Thus, this method will validate service config and mutate the
// passed mainNw to provide backward-compatibility whenever possible.
//
// It returns the container-wide MAC address, but this value will be kept empty for newer API versions.
func (s *composeService) prepareContainerMACAddress(service types.ServiceConfig, mainNw *types.ServiceNetworkConfig, nwName string) error {
// Engine API 1.44 added support for endpoint-specific MAC address and now returns a warning when a MAC address is
// set in container.Config. Thus, we have to jump through a number of hoops:
//
// 1. Top-level mac_address and main endpoint's MAC address should be the same ;
// 2. If supported by the API, top-level mac_address should be migrated to the main endpoint and container.Config
// should be kept empty ;
// 3. Otherwise, the endpoint mac_address should be set in container.Config and no other endpoint-specific
// mac_address can be specified. If that's the case, use top-level mac_address ;
//
// After that, if an endpoint mac_address is set, it's either user-defined or migrated by the code below, so
// there's no need to check for API version in defaultNetworkSettings.
macAddress := service.MacAddress
if macAddress != "" && mainNw != nil && mainNw.MacAddress != "" && mainNw.MacAddress != macAddress {
return fmt.Errorf("the service-level mac_address should have the same value as network %s", nwName)
}
if mainNw != nil && mainNw.MacAddress == "" {
mainNw.MacAddress = macAddress
}
return nil
}
func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, cfg *types.ServiceNetworkConfig, useNetworkAliases bool) []string {
aliases := []string{getContainerName(project.Name, service, serviceIndex)}
if useNetworkAliases {
aliases = append(aliases, service.Name)
if cfg != nil {
aliases = append(aliases, cfg.Aliases...)
}
}
return aliases
}
func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) (*network.EndpointSettings, error) {
const ifname = "com.docker.network.endpoint.ifname"
config := service.Networks[networkKey]
var ipam *network.EndpointIPAMConfig
var (
ipv4Address netip.Addr
ipv6Address netip.Addr
macAddress string
driverOpts types.Options
gwPriority int
)
if config != nil {
var err error
if config.Ipv4Address != "" {
ipv4Address, err = netip.ParseAddr(config.Ipv4Address)
if err != nil {
return nil, fmt.Errorf("invalid IPv4 address: %w", err)
}
}
if config.Ipv6Address != "" {
ipv6Address, err = netip.ParseAddr(config.Ipv6Address)
if err != nil {
return nil, fmt.Errorf("invalid IPv6 address: %w", err)
}
}
var linkLocalIPs []netip.Addr
for _, link := range config.LinkLocalIPs {
if link == "" {
continue
}
llIP, err := netip.ParseAddr(link)
if err != nil {
return nil, fmt.Errorf("invalid link-local IP: %w", err)
}
linkLocalIPs = append(linkLocalIPs, llIP)
}
ipam = &network.EndpointIPAMConfig{
IPv4Address: ipv4Address.Unmap(),
IPv6Address: ipv6Address,
LinkLocalIPs: linkLocalIPs,
}
macAddress = config.MacAddress
driverOpts = config.DriverOpts
if config.InterfaceName != "" {
if driverOpts == nil {
driverOpts = map[string]string{}
}
if name, ok := driverOpts[ifname]; ok && name != config.InterfaceName {
logrus.Warnf("ignoring services.%s.networks.%s.interface_name as %s driver_opts is already declared", service.Name, networkKey, ifname)
}
driverOpts[ifname] = config.InterfaceName
}
gwPriority = config.GatewayPriority
}
var ma network.HardwareAddr
if macAddress != "" {
var err error
ma, err = parseMACAddr(macAddress)
if err != nil {
return nil, err
}
}
return &network.EndpointSettings{
Aliases: getAliases(p, service, serviceIndex, config, useNetworkAliases),
Links: links,
IPAddress: ipv4Address,
IPv6Gateway: ipv6Address,
IPAMConfig: ipam,
MacAddress: ma,
DriverOpts: driverOpts,
GwPriority: gwPriority,
}, nil
}
// copy/pasted from https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697 + RelativePath
// TODO find so way to share this code with docker/cli
func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, bool, error) {
var (
unconfined bool
parsed []string
)
for _, opt := range securityOpts {
if opt == "systempaths=unconfined" {
unconfined = true
continue
}
con := strings.SplitN(opt, "=", 2)
if len(con) == 1 && con[0] != "no-new-privileges" {
if strings.Contains(opt, ":") {
con = strings.SplitN(opt, ":", 2)
} else {
return securityOpts, false, fmt.Errorf("invalid security-opt: %q", opt)
}
}
if con[0] == "seccomp" && con[1] != "unconfined" && con[1] != "builtin" {
f, err := os.ReadFile(p.RelativePath(con[1]))
if err != nil {
return securityOpts, false, fmt.Errorf("opening seccomp profile (%s) failed: %w", con[1], err)
}
b := bytes.NewBuffer(nil)
if err := json.Compact(b, f); err != nil {
return securityOpts, false, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", con[1], err)
}
parsed = append(parsed, fmt.Sprintf("seccomp=%s", b.Bytes()))
} else {
parsed = append(parsed, opt)
}
}
return parsed, unconfined, nil
}
func (s *composeService) prepareLabels(labels types.Labels, service types.ServiceConfig, number int) (map[string]string, error) {
hash, err := ServiceHash(service)
if err != nil {
return nil, err
}
labels[api.ConfigHashLabel] = hash
if number > 0 {
// One-off containers are not indexed
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
}
var dependencies []string
for s, d := range service.DependsOn {
dependencies = append(dependencies, fmt.Sprintf("%s:%s:%t", s, d.Condition, d.Restart))
}
labels[api.DependenciesLabel] = strings.Join(dependencies, ",")
return labels, nil
}
// defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
func defaultNetworkSettings(project *types.Project,
service types.ServiceConfig, serviceIndex int,
links []string, useNetworkAliases bool,
version string,
) (container.NetworkMode, *network.NetworkingConfig, error) {
if service.NetworkMode != "" {
return container.NetworkMode(service.NetworkMode), nil, nil
}
if len(project.Networks) == 0 {
return network.NetworkNone, nil, nil
}
if versions.LessThan(version, apiVersion149) {
for _, config := range service.Networks {
if config != nil && config.InterfaceName != "" {
return "", nil, fmt.Errorf("interface_name requires Docker Engine %s or later", DockerEngineV28_1)
}
}
}
serviceNetworks := service.NetworksByPriority()
primaryNetworkKey := "default"
if len(serviceNetworks) > 0 {
primaryNetworkKey = serviceNetworks[0]
serviceNetworks = serviceNetworks[1:]
}
primaryNetworkEndpoint, err := createEndpointSettings(project, service, serviceIndex, primaryNetworkKey, links, useNetworkAliases)
if err != nil {
return "", nil, err
}
if primaryNetworkEndpoint.MacAddress.String() == "" {
primaryNetworkEndpoint.MacAddress, err = parseMACAddr(service.MacAddress)
if err != nil {
return "", nil, err
}
}
primaryNetworkMobyNetworkName := project.Networks[primaryNetworkKey].Name
endpointsConfig := map[string]*network.EndpointSettings{
primaryNetworkMobyNetworkName: primaryNetworkEndpoint,
}
// Starting from API version 1.44, the Engine will take several EndpointsConfigs
// so we can pass all the extra networks we want the container to be connected to
// in the network configuration instead of connecting the container to each extra
// network individually after creation.
for _, networkKey := range serviceNetworks {
epSettings, err := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
if err != nil {
return "", nil, err
}
mobyNetworkName := project.Networks[networkKey].Name
endpointsConfig[mobyNetworkName] = epSettings
}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: endpointsConfig,
}
// From the Engine API docs:
// > Supported standard values are: bridge, host, none, and container:.
// > Any other value is taken as a custom network's name to which this container should connect to.
return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig, nil
}
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
var restart container.RestartPolicy
if service.Restart != "" {
name, num, ok := strings.Cut(service.Restart, ":")
var attempts int
if ok {
attempts, _ = strconv.Atoi(num)
}
restart = container.RestartPolicy{
Name: mapRestartPolicyCondition(name),
MaximumRetryCount: attempts,
}
}
if service.Deploy != nil && service.Deploy.RestartPolicy != nil {
policy := *service.Deploy.RestartPolicy
var attempts int
if policy.MaxAttempts != nil {
attempts = int(*policy.MaxAttempts)
}
restart = container.RestartPolicy{
Name: mapRestartPolicyCondition(policy.Condition),
MaximumRetryCount: attempts,
}
}
return restart
}
func mapRestartPolicyCondition(condition string) container.RestartPolicyMode {
// map definitions of deploy.restart_policy to engine definitions
switch condition {
case "none", "no":
return container.RestartPolicyDisabled
case "on-failure":
return container.RestartPolicyOnFailure
case "unless-stopped":
return container.RestartPolicyUnlessStopped
case "any", "always":
return container.RestartPolicyAlways
default:
return container.RestartPolicyMode(condition)
}
}
func getDeployResources(s types.ServiceConfig) container.Resources {
var swappiness *int64
if s.MemSwappiness != 0 {
val := int64(s.MemSwappiness)
swappiness = &val
}
resources := container.Resources{
CgroupParent: s.CgroupParent,
Memory: int64(s.MemLimit),
MemorySwap: int64(s.MemSwapLimit),
MemorySwappiness: swappiness,
MemoryReservation: int64(s.MemReservation),
OomKillDisable: &s.OomKillDisable,
CPUCount: s.CPUCount,
CPUPeriod: s.CPUPeriod,
CPUQuota: s.CPUQuota,
CPURealtimePeriod: s.CPURTPeriod,
CPURealtimeRuntime: s.CPURTRuntime,
CPUShares: s.CPUShares,
NanoCPUs: int64(s.CPUS * 1e9),
CPUPercent: int64(s.CPUPercent * 100),
CpusetCpus: s.CPUSet,
DeviceCgroupRules: s.DeviceCgroupRules,
}
if s.PidsLimit != 0 {
resources.PidsLimit = &s.PidsLimit
}
setBlkio(s.BlkioConfig, &resources)
if s.Deploy != nil {
setLimits(s.Deploy.Resources.Limits, &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,
CgroupPermissions: device.Permissions,
})
}
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
}
func toUlimits(m map[string]*types.UlimitsConfig) []*container.Ulimit {
var ulimits []*container.Ulimit
for name, u := range m {
soft := u.Single
if u.Soft != 0 {
soft = u.Soft
}
hard := u.Single
if u.Hard != 0 {
hard = u.Hard
}
ulimits = append(ulimits, &container.Ulimit{
Name: name,
Hard: int64(hard),
Soft: int64(soft),
})
}
return ulimits
}
func setReservations(reservations *types.Resource, resources *container.Resources) {
if reservations == nil {
return
}
// Cpu reservation is a swarm option and PIDs is only a limit
// So we only need to map memory reservation and devices
if reservations.MemoryBytes != 0 {
resources.MemoryReservation = int64(reservations.MemoryBytes)
}
for _, device := range reservations.Devices {
resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
Capabilities: [][]string{device.Capabilities},
Count: int(device.Count),
DeviceIDs: device.IDs,
Driver: device.Driver,
Options: device.Options,
})
}
}
func setLimits(limits *types.Resource, resources *container.Resources) {
if limits == nil {
return
}
if limits.MemoryBytes != 0 {
resources.Memory = int64(limits.MemoryBytes)
}
if limits.NanoCPUs != 0 {
resources.NanoCPUs = int64(limits.NanoCPUs * 1e9)
}
if limits.Pids > 0 {
resources.PidsLimit = &limits.Pids
}
}
func setBlkio(blkio *types.BlkioConfig, resources *container.Resources) {
if blkio == nil {
return
}
resources.BlkioWeight = blkio.Weight
for _, b := range blkio.WeightDevice {
resources.BlkioWeightDevice = append(resources.BlkioWeightDevice, &blkiodev.WeightDevice{
Path: b.Path,
Weight: b.Weight,
})
}
for _, b := range blkio.DeviceReadBps {
resources.BlkioDeviceReadBps = append(resources.BlkioDeviceReadBps, &blkiodev.ThrottleDevice{
Path: b.Path,
Rate: uint64(b.Rate),
})
}
for _, b := range blkio.DeviceReadIOps {
resources.BlkioDeviceReadIOps = append(resources.BlkioDeviceReadIOps, &blkiodev.ThrottleDevice{
Path: b.Path,
Rate: uint64(b.Rate),
})
}
for _, b := range blkio.DeviceWriteBps {
resources.BlkioDeviceWriteBps = append(resources.BlkioDeviceWriteBps, &blkiodev.ThrottleDevice{
Path: b.Path,
Rate: uint64(b.Rate),
})
}
for _, b := range blkio.DeviceWriteIOps {
resources.BlkioDeviceWriteIOps = append(resources.BlkioDeviceWriteIOps, &blkiodev.ThrottleDevice{
Path: b.Path,
Rate: uint64(b.Rate),
})
}
}
func buildContainerPorts(s types.ServiceConfig) (network.PortSet, error) {
// Add published ports as exposed ports.
exposedPorts := network.PortSet{}
for _, p := range s.Ports {
np, err := network.ParsePort(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
if err != nil {
return nil, err
}
exposedPorts[np] = struct{}{}
}
// Merge in exposed ports to the map of published ports
for _, e := range s.Expose {
// support two formats for expose, original format /[]
// or /[]
pr, err := network.ParsePortRange(e)
if err != nil {
return nil, err
}
// parse the start and end port and create a sequence of ports to expose
// if expose a port, the start and end port are the same
for p := range pr.All() {
exposedPorts[p] = struct{}{}
}
}
return exposedPorts, nil
}
func buildContainerPortBindingOptions(s types.ServiceConfig) (network.PortMap, error) {
bindings := network.PortMap{}
for _, port := range s.Ports {
var err error
p, err := network.ParsePort(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
if err != nil {
return nil, err
}
var hostIP netip.Addr
if port.HostIP != "" {
hostIP, err = netip.ParseAddr(port.HostIP)
if err != nil {
return nil, err
}
}
bindings[p] = append(bindings[p], network.PortBinding{
HostIP: hostIP,
HostPort: port.Published,
})
}
return bindings, nil
}
func getDependentServiceFromMode(mode string) string {
if strings.HasPrefix(
mode,
types.NetworkModeServicePrefix,
) {
return mode[len(types.NetworkModeServicePrefix):]
}
return ""
}
func (s *composeService) buildContainerVolumes(
ctx context.Context,
p types.Project,
service types.ServiceConfig,
inherit *container.Summary,
) ([]string, []mount.Mount, error) {
var mounts []mount.Mount
var binds []string
mountOptions, err := s.buildContainerMountOptions(ctx, p, service, inherit)
if err != nil {
return nil, nil, err
}
for _, m := range mountOptions {
switch m.Type {
case mount.TypeBind:
// `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
v := findVolumeByTarget(service.Volumes, m.Target)
if v != nil {
if v.Type != types.VolumeTypeBind {
v.Source = m.Source
}
if !bindRequiresMountAPI(v.Bind) {
source := m.Source
if vol := findVolumeByName(p.Volumes, m.Source); vol != nil {
source = m.Source
}
binds = append(binds, toBindString(source, v))
continue
}
}
case mount.TypeVolume:
v := findVolumeByTarget(service.Volumes, m.Target)
vol := findVolumeByName(p.Volumes, m.Source)
if v != nil && vol != nil {
// Prefer the bind API if no advanced option is used, to preserve backward compatibility
if !volumeRequiresMountAPI(v.Volume) {
binds = append(binds, toBindString(vol.Name, v))
continue
}
}
case mount.TypeImage:
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, nil, err
}
if versions.LessThan(version, apiVersion148) {
return nil, nil, fmt.Errorf("volume with type=image require Docker Engine %s or later", dockerEngineV28)
}
}
mounts = append(mounts, m)
}
return binds, mounts, nil
}
func toBindString(name string, v *types.ServiceVolumeConfig) string {
access := "rw"
if v.ReadOnly {
access = "ro"
}
options := []string{access}
if v.Bind != nil && v.Bind.SELinux != "" {
options = append(options, v.Bind.SELinux)
}
if v.Bind != nil && v.Bind.Propagation != "" {
options = append(options, v.Bind.Propagation)
}
if v.Volume != nil && v.Volume.NoCopy {
options = append(options, "nocopy")
}
return fmt.Sprintf("%s:%s:%s", name, v.Target, strings.Join(options, ","))
}
func findVolumeByName(volumes types.Volumes, name string) *types.VolumeConfig {
for _, vol := range volumes {
if vol.Name == name {
return &vol
}
}
return nil
}
func findVolumeByTarget(volumes []types.ServiceVolumeConfig, target string) *types.ServiceVolumeConfig {
for _, v := range volumes {
if v.Target == target {
return &v
}
}
return nil
}
// bindRequiresMountAPI 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 bindRequiresMountAPI(bind *types.ServiceVolumeBind) bool {
switch {
case bind == nil:
return false
case !bool(bind.CreateHostPath):
return true
case bind.Propagation != "":
return true
case bind.Recursive != "":
return true
default:
return false
}
}
// volumeRequiresMountAPI check if Volume declaration can be implemented by the plain old Bind API or uses any of the advanced
// options which require use of Mount API
func volumeRequiresMountAPI(vol *types.ServiceVolumeVolume) bool {
switch {
case vol == nil:
return false
case len(vol.Labels) > 0:
return true
case vol.Subpath != "":
return true
case vol.NoCopy:
return true
default:
return false
}
}
func (s *composeService) buildContainerMountOptions(ctx context.Context, p types.Project, service types.ServiceConfig, inherit *container.Summary) ([]mount.Mount, error) {
mounts := map[string]mount.Mount{}
if inherit != nil {
for _, m := range inherit.Mounts {
if m.Type == "tmpfs" {
continue
}
src := m.Source
if m.Type == "volume" {
src = m.Name
}
img, err := s.apiClient().ImageInspect(ctx, api.GetImageNameOrDefault(service, p.Name))
if err != nil {
return nil, err
}
if img.Config != nil {
if _, ok := img.Config.Volumes[m.Destination]; ok {
// inherit previous container's anonymous volume
mounts[m.Destination] = mount.Mount{
Type: m.Type,
Source: src,
Target: m.Destination,
ReadOnly: !m.RW,
}
}
}
volumes := []types.ServiceVolumeConfig{}
for _, v := range service.Volumes {
if v.Target != m.Destination || v.Source != "" {
volumes = append(volumes, v)
continue
}
// inherit previous container's anonymous volume
mounts[m.Destination] = mount.Mount{
Type: m.Type,
Source: src,
Target: m.Destination,
ReadOnly: !m.RW,
}
}
service.Volumes = volumes
}
}
mounts, err := fillBindMounts(p, service, mounts)
if err != nil {
return nil, err
}
values := make([]mount.Mount, 0, len(mounts))
for _, v := range mounts {
values = append(values, v)
}
return values, nil
}
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 := buildContainerSecretMounts(p, s)
if err != nil {
return nil, err
}
for _, s := range secrets {
if _, found := m[s.Target]; found {
continue
}
m[s.Target] = s
}
configs, err := buildContainerConfigMounts(p, s)
if err != nil {
return nil, err
}
for _, c := range configs {
if _, found := m[c.Target]; found {
continue
}
m[c.Target] = c
}
return m, nil
}
func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
mounts := map[string]mount.Mount{}
configsBaseDir := "/"
for _, config := range s.Configs {
target := config.Target
if config.Target == "" {
target = configsBaseDir + config.Source
} else if !isAbsTarget(config.Target) {
target = configsBaseDir + config.Target
}
definedConfig := p.Configs[config.Source]
if definedConfig.External {
return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name)
}
if definedConfig.Driver != "" {
return nil, errors.New("Docker Compose does not support configs.*.driver") //nolint:staticcheck
}
if definedConfig.TemplateDriver != "" {
return nil, errors.New("Docker Compose does not support configs.*.template_driver") //nolint:staticcheck
}
if definedConfig.Environment != "" || definedConfig.Content != "" {
continue
}
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,
ReadOnly: true,
})
if err != nil {
return nil, err
}
mounts[target] = bindMount
}
values := make([]mount.Mount, 0, len(mounts))
for _, v := range mounts {
values = append(values, v)
}
return values, nil
}
func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
mounts := map[string]mount.Mount{}
secretsDir := "/run/secrets/"
for _, secret := range s.Secrets {
target := secret.Target
if secret.Target == "" {
target = secretsDir + secret.Source
} else if !isAbsTarget(secret.Target) {
target = secretsDir + secret.Target
}
definedSecret := p.Secrets[secret.Source]
if definedSecret.External {
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
}
if definedSecret.Driver != "" {
return nil, errors.New("Docker Compose does not support secrets.*.driver") //nolint:staticcheck
}
if definedSecret.TemplateDriver != "" {
return nil, errors.New("Docker Compose does not support secrets.*.template_driver") //nolint:staticcheck
}
if definedSecret.Environment != "" {
continue
}
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
}
mounts[target] = mnt
}
values := make([]mount.Mount, 0, len(mounts))
for _, v := range mounts {
values = append(values, v)
}
return values, nil
}
func isAbsTarget(p string) bool {
return isUnixAbs(p) || isWindowsAbs(p)
}
func isUnixAbs(p string) bool {
return strings.HasPrefix(p, "/")
}
func isWindowsAbs(p string) bool {
return paths.IsWindowsAbs(p)
}
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
source := volume.Source
switch volume.Type {
case types.VolumeTypeBind:
if !filepath.IsAbs(source) && !isUnixAbs(source) && !isWindowsAbs(source) {
// volume source has already been prefixed with workdir if required, by compose-go project loader
var err error
source, err = filepath.Abs(source)
if err != nil {
return mount.Mount{}, err
}
}
case types.VolumeTypeVolume:
if volume.Source != "" {
pVolume, ok := project.Volumes[volume.Source]
if ok {
source = pVolume.Name
}
}
}
bind, vol, tmpfs, img := buildMountOptions(volume)
if bind != nil {
volume.Type = types.VolumeTypeBind
}
return mount.Mount{
Type: mount.Type(volume.Type),
Source: source,
Target: volume.Target,
ReadOnly: volume.ReadOnly,
Consistency: mount.Consistency(volume.Consistency),
BindOptions: bind,
VolumeOptions: vol,
TmpfsOptions: tmpfs,
ImageOptions: img,
}, nil
}
func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, *mount.ImageOptions) {
if volume.Type != types.VolumeTypeBind && volume.Bind != nil {
logrus.Warnf("mount of type `%s` should not define `bind` option", volume.Type)
}
if volume.Type != types.VolumeTypeVolume && volume.Volume != nil {
logrus.Warnf("mount of type `%s` should not define `volume` option", volume.Type)
}
if volume.Type != types.VolumeTypeTmpfs && volume.Tmpfs != nil {
logrus.Warnf("mount of type `%s` should not define `tmpfs` option", volume.Type)
}
if volume.Type != types.VolumeTypeImage && volume.Image != nil {
logrus.Warnf("mount of type `%s` should not define `image` option", volume.Type)
}
switch volume.Type {
case "bind":
return buildBindOption(volume.Bind), nil, nil, nil
case "volume":
return nil, buildVolumeOptions(volume.Volume), nil, nil
case "tmpfs":
return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil
case "image":
return nil, nil, nil, buildImageOptions(volume.Image)
}
return nil, nil, nil, nil
}
func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
if bind == nil {
return nil
}
opts := &mount.BindOptions{
Propagation: mount.Propagation(bind.Propagation),
CreateMountpoint: bool(bind.CreateHostPath),
}
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 {
if vol == nil {
return nil
}
return &mount.VolumeOptions{
NoCopy: vol.NoCopy,
Subpath: vol.Subpath,
Labels: vol.Labels,
// DriverConfig: , // FIXME missing from model ?
}
}
func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
if tmpfs == nil {
return nil
}
return &mount.TmpfsOptions{
SizeBytes: int64(tmpfs.Size),
Mode: os.FileMode(tmpfs.Mode),
}
}
func buildImageOptions(image *types.ServiceVolumeImage) *mount.ImageOptions {
if image == nil {
return nil
}
return &mount.ImageOptions{
Subpath: image.SubPath,
}
}
func (s *composeService) ensureNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) {
if n.External {
return s.resolveExternalNetwork(ctx, n)
}
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
if errdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, project, name, n)
}
return id, err
}
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) { //nolint:gocyclo
// This is containers that could be left after a diverged network was removed
var dangledContainers Containers
// First, try to find a unique network matching by name or ID
res, err := s.apiClient().NetworkInspect(ctx, n.Name, client.NetworkInspectOptions{})
if err == nil {
inspect := res.Network
// NetworkInspect will match on ID prefix, so double check we get the expected one
// as looking for network named `db` we could erroneously match network ID `db9086999caf`
if inspect.Name == n.Name || inspect.ID == n.Name {
p, ok := inspect.Labels[api.ProjectLabel]
if !ok {
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
"Set `external: true` to use an existing network", n.Name)
} else if p != project.Name {
logrus.Warnf("a network with name %s exists but was not created for project %q.\n"+
"Set `external: true` to use an existing network", n.Name, project.Name)
}
if inspect.Labels[api.NetworkLabel] != name {
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],
name,
)
}
hash := inspect.Labels[api.ConfigHashLabel]
expected, err := NetworkHash(n)
if err != nil {
return "", err
}
if hash == "" || hash == expected {
return inspect.ID, nil
}
dangledContainers, err = s.removeDivergedNetwork(ctx, project, name, n)
if err != nil {
return "", err
}
}
}
// ignore other errors. Typically, an ambiguous request by name results in some generic `invalidParameter` error
// Either not found, or name is ambiguous - use NetworkList to list by name
nwList, err := s.apiClient().NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add("name", n.Name),
})
if err != nil {
return "", err
}
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
networks := slices.DeleteFunc(nwList.Items, func(net network.Summary) bool {
return net.Name != n.Name
})
for _, nw := range networks {
if nw.Labels[api.ProjectLabel] == project.Name &&
nw.Labels[api.NetworkLabel] == name {
return nw.ID, nil
}
}
// we could have set NetworkList with a projectFilter and networkFilter but not doing so allows to catch this
// scenario were a network with same name exists but doesn't have label, and use of `CheckDuplicate: true`
// prevents to create another one.
if len(networks) > 0 {
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
"Set `external: true` to use an existing network", n.Name)
return networks[0].ID, nil
}
var ipam *network.IPAM
if n.Ipam.Config != nil {
var config []network.IPAMConfig
for _, pool := range n.Ipam.Config {
c, err := parseIPAMPool(pool)
if err != nil {
return "", err
}
config = append(config, c)
}
ipam = &network.IPAM{
Driver: n.Ipam.Driver,
Config: config,
}
}
hash, err := NetworkHash(n)
if err != nil {
return "", err
}
n.CustomLabels = n.CustomLabels.Add(api.ConfigHashLabel, hash)
createOpts := client.NetworkCreateOptions{
Labels: mergeLabels(n.Labels, n.CustomLabels),
Driver: n.Driver,
Options: n.DriverOpts,
Internal: n.Internal,
Attachable: n.Attachable,
IPAM: ipam,
EnableIPv6: n.EnableIPv6,
EnableIPv4: n.EnableIPv4,
}
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
createOpts.IPAM = &network.IPAM{}
}
if n.Ipam.Driver != "" {
createOpts.IPAM.Driver = n.Ipam.Driver
}
for _, ipamConfig := range n.Ipam.Config {
c, err := parseIPAMPool(ipamConfig)
if err != nil {
return "", err
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, c)
}
networkEventName := fmt.Sprintf("Network %s", n.Name)
s.events.On(creatingEvent(networkEventName))
resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
if err != nil {
s.events.On(errorEvent(networkEventName, err.Error()))
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
}
s.events.On(createdEvent(networkEventName))
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
if err != nil {
return "", err
}
return resp.ID, nil
}
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (Containers, error) {
// Remove services attached to this network to force recreation
var services []string
for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool {
_, ok := config.Networks[name]
return ok
}) {
services = append(services, service.Name)
}
// Stop containers so we can remove network
// They will be restarted (actually: recreated) with the updated network
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
}, nil)
if err != nil {
return nil, err
}
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...)
if err != nil {
return nil, err
}
err = s.disconnectNetwork(ctx, n.Name, containers)
if err != nil {
return nil, err
}
_, err = s.apiClient().NetworkRemove(ctx, n.Name, client.NetworkRemoveOptions{})
eventName := fmt.Sprintf("Network %s", n.Name)
s.events.On(removedEvent(eventName))
return containers, err
}
func (s *composeService) disconnectNetwork(
ctx context.Context,
nwName string,
containers Containers,
) error {
for _, c := range containers {
_, err := s.apiClient().NetworkDisconnect(ctx, nwName, client.NetworkDisconnectOptions{
Container: c.ID,
Force: true,
})
if err != nil {
return err
}
}
return nil
}
func (s *composeService) connectNetwork(
ctx context.Context,
nwName string,
containers Containers,
config *network.EndpointSettings,
) error {
for _, c := range containers {
_, err := s.apiClient().NetworkConnect(ctx, nwName, client.NetworkConnectOptions{
Container: c.ID,
EndpointConfig: config,
})
if err != nil {
return err
}
}
return nil
}
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) (string, error) {
// NetworkInspect will match on ID prefix, so NetworkList with a name
// filter is used to look for an exact match to prevent e.g. a network
// named `db` from getting erroneously matched to a network with an ID
// like `db9086999caf`
res, err := s.apiClient().NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add("name", n.Name),
})
if err != nil {
return "", err
}
networks := res.Items
if len(networks) == 0 {
// in this instance, n.Name is really an ID
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, client.NetworkInspectOptions{})
if err == nil {
networks = append(networks, network.Summary{Network: sn.Network.Network})
} else if !errdefs.IsNotFound(err) {
return "", err
}
}
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
networks = slices.DeleteFunc(networks, func(net network.Summary) bool {
// this function is called during the rebuild stage of `compose watch`.
// we still require just one network back, but we need to run the search on the ID
return net.Name != n.Name && net.ID != n.Name
})
switch len(networks) {
case 1:
return networks[0].ID, nil
case 0:
enabled, err := s.isSwarmEnabled(ctx)
if err != nil {
return "", err
}
if enabled {
// Swarm nodes do not register overlay networks that were
// created on a different node unless they're in use.
// So we can't preemptively check network exists, but
// networkAttach will later fail anyway if network actually doesn't exist
return "swarm", nil
}
return "", fmt.Errorf("network %s declared as external, but could not be found", n.Name)
default:
return "", fmt.Errorf("multiple networks with name %q were found. Use network ID as `name` to avoid ambiguity", n.Name)
}
}
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) (string, error) {
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name, client.VolumeInspectOptions{})
if err != nil {
if !errdefs.IsNotFound(err) {
return "", err
}
if volume.External {
return "", fmt.Errorf("external volume %q not found", volume.Name)
}
err = s.createVolume(ctx, volume)
return volume.Name, err
}
if volume.External {
return volume.Name, nil
}
// Volume exists with name, but let's double-check this is the expected one
p, ok := inspected.Volume.Labels[api.ProjectLabel]
if !ok {
logrus.Warnf("volume %q already exists but was not created by Docker Compose. Use `external: true` to use an existing volume", volume.Name)
}
if ok && p != project.Name {
logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project.Name)
}
expected, err := VolumeHash(volume)
if err != nil {
return "", err
}
actual, ok := inspected.Volume.Labels[api.ConfigHashLabel]
if ok && actual != expected {
msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name)
confirm, err := s.prompt(msg, false)
if err != nil {
return "", err
}
if confirm {
err = s.removeDivergedVolume(ctx, name, volume, project)
if err != nil {
return "", err
}
return volume.Name, s.createVolume(ctx, volume)
}
}
return inspected.Volume.Name, nil
}
func (s *composeService) removeDivergedVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) error {
// Remove services mounting divergent volume
var services []string
for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool {
for _, cfg := range config.Volumes {
if cfg.Source == name {
return true
}
}
return false
}) {
services = append(services, service.Name)
}
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
}, nil)
if err != nil {
return err
}
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...)
if err != nil {
return err
}
// FIXME (ndeloof) we have to remove container so we can recreate volume
// but doing so we can't inherit anonymous volumes from previous instance
err = s.remove(ctx, containers, api.RemoveOptions{
Services: services,
Project: project,
})
if err != nil {
return err
}
_, err = s.apiClient().VolumeRemove(ctx, volume.Name, client.VolumeRemoveOptions{
Force: true,
})
return err
}
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
eventName := fmt.Sprintf("Volume %s", volume.Name)
s.events.On(creatingEvent(eventName))
hash, err := VolumeHash(volume)
if err != nil {
return err
}
volume.CustomLabels.Add(api.ConfigHashLabel, hash)
_, err = s.apiClient().VolumeCreate(ctx, client.VolumeCreateOptions{
Labels: mergeLabels(volume.Labels, volume.CustomLabels),
Name: volume.Name,
Driver: volume.Driver,
DriverOpts: volume.DriverOpts,
})
if err != nil {
s.events.On(errorEvent(eventName, err.Error()))
return err
}
s.events.On(createdEvent(eventName))
return nil
}
func parseIPAMPool(pool *types.IPAMPool) (network.IPAMConfig, error) {
var (
err error
subNet netip.Prefix
ipRange netip.Prefix
gateway netip.Addr
auxAddress map[string]netip.Addr
)
if pool.Subnet != "" {
subNet, err = netip.ParsePrefix(pool.Subnet)
if err != nil {
return network.IPAMConfig{}, fmt.Errorf("invalid subnet: %w", err)
}
}
if pool.IPRange != "" {
ipRange, err = netip.ParsePrefix(pool.IPRange)
if err != nil {
return network.IPAMConfig{}, fmt.Errorf("invalid ip-range: %w", err)
}
}
if pool.Gateway != "" {
gateway, err = netip.ParseAddr(pool.Gateway)
if err != nil {
return network.IPAMConfig{}, fmt.Errorf("invalid gateway address: %w", err)
}
}
if len(pool.AuxiliaryAddresses) > 0 {
auxAddress = make(map[string]netip.Addr, len(pool.AuxiliaryAddresses))
for auxName, addr := range pool.AuxiliaryAddresses {
auxAddr, err := netip.ParseAddr(addr)
if err != nil {
return network.IPAMConfig{}, fmt.Errorf("invalid auxiliary address: %w", err)
}
auxAddress[auxName] = auxAddr
}
}
return network.IPAMConfig{
Subnet: subNet,
IPRange: ipRange,
Gateway: gateway,
AuxAddress: auxAddress,
}, nil
}
func parseMACAddr(macAddress string) (network.HardwareAddr, error) {
if macAddress == "" {
return nil, nil
}
m, err := net.ParseMAC(macAddress)
if err != nil {
return nil, fmt.Errorf("invalid MAC address: %w", err)
}
return network.HardwareAddr(m), nil
}
================================================
FILE: pkg/compose/create_test.go
================================================
/*
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 (
"net"
"net/netip"
"os"
"path/filepath"
"sort"
"testing"
composeloader "github.com/compose-spec/compose-go/v2/loader"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
mountTypes "github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"github.com/docker/compose/v5/pkg/api"
)
func TestBuildBindMount(t *testing.T) {
project := composetypes.Project{}
volume := composetypes.ServiceVolumeConfig{
Type: composetypes.VolumeTypeBind,
Source: "",
Target: "/data",
}
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Assert(t, filepath.IsAbs(mount.Source))
_, err = os.Stat(mount.Source)
assert.NilError(t, err)
assert.Equal(t, mount.Type, mountTypes.TypeBind)
}
func TestBuildNamedPipeMount(t *testing.T) {
project := composetypes.Project{}
volume := composetypes.ServiceVolumeConfig{
Type: composetypes.VolumeTypeNamedPipe,
Source: "\\\\.\\pipe\\docker_engine_windows",
Target: "\\\\.\\pipe\\docker_engine",
}
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Equal(t, mount.Type, mountTypes.TypeNamedPipe)
}
func TestBuildVolumeMount(t *testing.T) {
project := composetypes.Project{
Name: "myProject",
Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{
"myVolume": {
Name: "myProject_myVolume",
},
}),
}
volume := composetypes.ServiceVolumeConfig{
Type: composetypes.VolumeTypeVolume,
Source: "myVolume",
Target: "/data",
}
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Equal(t, mount.Source, "myProject_myVolume")
assert.Equal(t, mount.Type, mountTypes.TypeVolume)
}
func TestServiceImageName(t *testing.T) {
assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Image: "myImage"}, "myProject"), "myImage")
assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Name: "aService"}, "myProject"), "myProject-aService")
}
func TestPrepareNetworkLabels(t *testing.T) {
project := composetypes.Project{
Name: "myProject",
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{"skynet": {}}),
}
prepareNetworks(&project)
assert.DeepEqual(t, project.Networks["skynet"].CustomLabels, composetypes.Labels(map[string]string{
"com.docker.compose.network": "skynet",
"com.docker.compose.project": "myProject",
"com.docker.compose.version": api.ComposeVersion,
}))
}
func TestBuildContainerMountOptions(t *testing.T) {
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{
"myService": {
Name: "myService",
Volumes: []composetypes.ServiceVolumeConfig{
{
Type: composetypes.VolumeTypeVolume,
Target: "/var/myvolume1",
},
{
Type: composetypes.VolumeTypeVolume,
Target: "/var/myvolume2",
},
{
Type: composetypes.VolumeTypeVolume,
Source: "myVolume3",
Target: "/var/myvolume3",
Volume: &composetypes.ServiceVolumeVolume{
Subpath: "etc",
},
},
{
Type: composetypes.VolumeTypeNamedPipe,
Source: "\\\\.\\pipe\\docker_engine_windows",
Target: "\\\\.\\pipe\\docker_engine",
},
},
},
},
Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{
"myVolume1": {
Name: "myProject_myVolume1",
},
"myVolume2": {
Name: "myProject_myVolume2",
},
}),
}
inherit := &container.Summary{
Mounts: []container.MountPoint{
{
Type: composetypes.VolumeTypeVolume,
Destination: "/var/myvolume1",
},
{
Type: composetypes.VolumeTypeVolume,
Destination: "/var/myvolume2",
},
},
}
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mock, cli := prepareMocks(mockCtrl)
s := composeService{
dockerCli: cli,
}
mock.EXPECT().ImageInspect(gomock.Any(), "myProject-myService").AnyTimes().Return(client.ImageInspectResult{}, nil)
mounts, err := s.buildContainerMountOptions(t.Context(), project, project.Services["myService"], inherit)
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Target < mounts[j].Target
})
assert.NilError(t, err)
assert.Assert(t, len(mounts) == 4)
assert.Equal(t, mounts[0].Target, "/var/myvolume1")
assert.Equal(t, mounts[1].Target, "/var/myvolume2")
assert.Equal(t, mounts[2].Target, "/var/myvolume3")
assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
mounts, err = s.buildContainerMountOptions(t.Context(), project, project.Services["myService"], inherit)
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Target < mounts[j].Target
})
assert.NilError(t, err)
assert.Assert(t, len(mounts) == 4)
assert.Equal(t, mounts[0].Target, "/var/myvolume1")
assert.Equal(t, mounts[1].Target, "/var/myvolume2")
assert.Equal(t, mounts[2].Target, "/var/myvolume3")
assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
}
func TestDefaultNetworkSettings(t *testing.T) {
t.Run("returns the network with the highest priority as primary when service has multiple networks", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"myNetwork1": {
Priority: 10,
},
"myNetwork2": {
Priority: 1000,
},
},
}
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{
"myService": service,
},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"myNetwork1": {
Name: "myProject_myNetwork1",
},
"myNetwork2": {
Name: "myProject_myNetwork2",
},
}),
}
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.44")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "myProject_myNetwork2")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 2))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork1"))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
})
t.Run("returns default network when service has no networks", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
}
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{
"myService": service,
},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"myNetwork1": {
Name: "myProject_myNetwork1",
},
"myNetwork2": {
Name: "myProject_myNetwork2",
},
"default": {
Name: "myProject_default",
},
}),
}
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.44")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "myProject_default")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
})
t.Run("returns none if project has no networks", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
}
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{
"myService": service,
},
}
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.44")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "none")
assert.Check(t, cmp.Nil(networkConfig))
})
t.Run("returns defined network mode if explicitly set", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
NetworkMode: "host",
}
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{"myService": service},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"default": {
Name: "myProject_default",
},
}),
}
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.44")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "host")
assert.Check(t, cmp.Nil(networkConfig))
})
}
func TestCreateEndpointSettings(t *testing.T) {
eps, err := createEndpointSettings(&composetypes.Project{
Name: "projName",
}, composetypes.ServiceConfig{
Name: "serviceName",
ContainerName: "containerName",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"netName": {
Priority: 100,
Aliases: []string{"alias1", "alias2"},
Ipv4Address: "10.16.17.18",
Ipv6Address: "fdb4:7a7f:373a:3f0c::42",
LinkLocalIPs: []string{"169.254.10.20"},
MacAddress: "02:00:00:00:00:01",
DriverOpts: composetypes.Options{
"driverOpt1": "optval1",
"driverOpt2": "optval2",
},
},
},
}, 0, "netName", []string{"link1", "link2"}, true)
assert.NilError(t, err)
macAddr, _ := net.ParseMAC("02:00:00:00:00:01")
assert.Check(t, cmp.DeepEqual(eps, &network.EndpointSettings{
IPAMConfig: &network.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("10.16.17.18").Unmap(),
IPv6Address: netip.MustParseAddr("fdb4:7a7f:373a:3f0c::42"),
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.10.20").Unmap()},
},
Links: []string{"link1", "link2"},
Aliases: []string{"containerName", "serviceName", "alias1", "alias2"},
MacAddress: network.HardwareAddr(macAddr),
DriverOpts: map[string]string{
"driverOpt1": "optval1",
"driverOpt2": "optval2",
},
// FIXME(robmry) - IPAddress and IPv6Gateway are "operational data" fields...
// - The IPv6 address here is the container's address, not the gateway.
// - Both fields will be cleared by the daemon, but they could be removed from
// the request.
IPAddress: netip.MustParseAddr("10.16.17.18").Unmap(),
IPv6Gateway: netip.MustParseAddr("fdb4:7a7f:373a:3f0c::42"),
}, cmpopts.EquateComparable(netip.Addr{})))
}
func Test_buildContainerVolumes(t *testing.T) {
pwd, err := os.Getwd()
assert.NilError(t, err)
tests := []struct {
name string
yaml string
binds []string
mounts []mountTypes.Mount
}{
{
name: "bind mount local path",
yaml: `
services:
test:
volumes:
- ./data:/data
`,
binds: []string{filepath.Join(pwd, "data") + ":/data:rw"},
mounts: nil,
},
{
name: "bind mount, not create host path",
yaml: `
services:
test:
volumes:
- type: bind
source: ./data
target: /data
bind:
create_host_path: false
`,
binds: nil,
mounts: []mountTypes.Mount{
{
Type: "bind",
Source: filepath.Join(pwd, "data"),
Target: "/data",
BindOptions: &mountTypes.BindOptions{CreateMountpoint: false},
},
},
},
{
name: "mount volume",
yaml: `
services:
test:
volumes:
- data:/data
volumes:
data:
name: my_volume
`,
binds: []string{"my_volume:/data:rw"},
mounts: nil,
},
{
name: "mount volume, readonly",
yaml: `
services:
test:
volumes:
- data:/data:ro
volumes:
data:
name: my_volume
`,
binds: []string{"my_volume:/data:ro"},
mounts: nil,
},
{
name: "mount volume subpath",
yaml: `
services:
test:
volumes:
- type: volume
source: data
target: /data
volume:
subpath: test/
volumes:
data:
name: my_volume
`,
binds: nil,
mounts: []mountTypes.Mount{
{
Type: "volume",
Source: "my_volume",
Target: "/data",
VolumeOptions: &mountTypes.VolumeOptions{Subpath: "test/"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := composeloader.LoadWithContext(t.Context(), composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{
{
Filename: "test",
Content: []byte(tt.yaml),
},
},
}, func(options *composeloader.Options) {
options.SkipValidation = true
options.SkipConsistencyCheck = true
})
assert.NilError(t, err)
s := &composeService{}
binds, mounts, err := s.buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
assert.NilError(t, err)
assert.DeepEqual(t, tt.binds, binds)
assert.DeepEqual(t, tt.mounts, mounts)
})
}
}
================================================
FILE: pkg/compose/dependencies.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
// ServiceStatus indicates the status of a service
type ServiceStatus int
// Services status flags
const (
ServiceStopped ServiceStatus = iota
ServiceStarted
)
type graphTraversal struct {
mu sync.Mutex
seen map[string]struct{}
ignored map[string]struct{}
extremityNodesFn func(*Graph) []*Vertex // leaves or roots
adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren
filterAdjacentByStatusFn func(*Graph, string, ServiceStatus) []*Vertex // filterChildren or filterParents
targetServiceStatus ServiceStatus
adjacentServiceStatusToSkip ServiceStatus
visitorFn func(context.Context, string) error
maxConcurrency int
}
func upDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal {
return &graphTraversal{
extremityNodesFn: leaves,
adjacentNodesFn: getParents,
filterAdjacentByStatusFn: filterChildren,
adjacentServiceStatusToSkip: ServiceStopped,
targetServiceStatus: ServiceStarted,
visitorFn: visitorFn,
}
}
func downDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal {
return &graphTraversal{
extremityNodesFn: roots,
adjacentNodesFn: getChildren,
filterAdjacentByStatusFn: filterParents,
adjacentServiceStatusToSkip: ServiceStarted,
targetServiceStatus: ServiceStopped,
visitorFn: visitorFn,
}
}
// InDependencyOrder applies the function to the services of the project taking in account the dependency order
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
graph, err := NewGraph(project, ServiceStopped)
if err != nil {
return err
}
t := upDirectionTraversal(fn)
for _, option := range options {
option(t)
}
return t.visit(ctx, graph)
}
// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
graph, err := NewGraph(project, ServiceStarted)
if err != nil {
return err
}
t := downDirectionTraversal(fn)
for _, option := range options {
option(t)
}
return t.visit(ctx, graph)
}
func WithRootNodesAndDown(nodes []string) func(*graphTraversal) {
return func(t *graphTraversal) {
if len(nodes) == 0 {
return
}
originalFn := t.extremityNodesFn
t.extremityNodesFn = func(graph *Graph) []*Vertex {
var want []string
for _, node := range nodes {
vertex := graph.Vertices[node]
want = append(want, vertex.Service)
for _, v := range getAncestors(vertex) {
want = append(want, v.Service)
}
}
t.ignored = map[string]struct{}{}
for k := range graph.Vertices {
if !slices.Contains(want, k) {
t.ignored[k] = struct{}{}
}
}
return originalFn(graph)
}
}
}
func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
expect := len(g.Vertices)
if expect == 0 {
return nil
}
eg, ctx := errgroup.WithContext(ctx)
if t.maxConcurrency > 0 {
eg.SetLimit(t.maxConcurrency + 1)
}
nodeCh := make(chan *Vertex, expect)
defer close(nodeCh)
// nodeCh need to allow n=expect writers while reader goroutine could have returner after ctx.Done
eg.Go(func() error {
for {
select {
case <-ctx.Done():
return nil
case node := <-nodeCh:
expect--
if expect == 0 {
return nil
}
t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh)
}
}
})
nodes := t.extremityNodesFn(g)
t.run(ctx, g, eg, nodes, nodeCh)
return eg.Wait()
}
// Note: this could be `graph.walk` or whatever
func (t *graphTraversal) run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, nodeCh chan *Vertex) {
for _, node := range nodes {
// Don't start this service yet if all of its children have
// not been started yet.
if len(t.filterAdjacentByStatusFn(graph, node.Key, t.adjacentServiceStatusToSkip)) != 0 {
continue
}
if !t.consume(node.Key) {
// another worker already visited this node
continue
}
eg.Go(func() error {
var err error
if _, ignore := t.ignored[node.Service]; !ignore {
err = t.visitorFn(ctx, node.Service)
}
if err == nil {
graph.UpdateStatus(node.Key, t.targetServiceStatus)
}
nodeCh <- node
return err
})
}
}
func (t *graphTraversal) consume(nodeKey string) bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.seen == nil {
t.seen = make(map[string]struct{})
}
if _, ok := t.seen[nodeKey]; ok {
return false
}
t.seen[nodeKey] = struct{}{}
return true
}
// Graph represents project as service dependencies
type Graph struct {
Vertices map[string]*Vertex
lock sync.RWMutex
}
// Vertex represents a service in the dependencies structure
type Vertex struct {
Key string
Service string
Status ServiceStatus
Children map[string]*Vertex
Parents map[string]*Vertex
}
func getParents(v *Vertex) []*Vertex {
return v.GetParents()
}
// GetParents returns a slice with the parent vertices of the Vertex
func (v *Vertex) GetParents() []*Vertex {
var res []*Vertex
for _, p := range v.Parents {
res = append(res, p)
}
return res
}
func getChildren(v *Vertex) []*Vertex {
return v.GetChildren()
}
// getAncestors return all descendents for a vertex, might contain duplicates
func getAncestors(v *Vertex) []*Vertex {
var descendents []*Vertex
for _, parent := range v.GetParents() {
descendents = append(descendents, parent)
descendents = append(descendents, getAncestors(parent)...)
}
return descendents
}
// GetChildren returns a slice with the child vertices of the Vertex
func (v *Vertex) GetChildren() []*Vertex {
var res []*Vertex
for _, p := range v.Children {
res = append(res, p)
}
return res
}
// NewGraph returns the dependency graph of the services
func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, error) {
graph := &Graph{
lock: sync.RWMutex{},
Vertices: map[string]*Vertex{},
}
for _, s := range project.Services {
graph.AddVertex(s.Name, s.Name, initialStatus)
}
for index, s := range project.Services {
for _, name := range s.GetDependencies() {
err := graph.AddEdge(s.Name, name)
if err != nil {
if !s.DependsOn[name].Required {
delete(s.DependsOn, name)
project.Services[index] = s
continue
}
if api.IsNotFoundError(err) {
ds, err := project.GetDisabledService(name)
if err == nil {
return nil, fmt.Errorf("service %s is required by %s but is disabled. Can be enabled by profiles %s", name, s.Name, ds.Profiles)
}
}
return nil, err
}
}
}
if b, err := graph.HasCycles(); b {
return nil, err
}
return graph, nil
}
// NewVertex is the constructor function for the Vertex
func NewVertex(key string, service string, initialStatus ServiceStatus) *Vertex {
return &Vertex{
Key: key,
Service: service,
Status: initialStatus,
Parents: map[string]*Vertex{},
Children: map[string]*Vertex{},
}
}
// AddVertex adds a vertex to the Graph
func (g *Graph) AddVertex(key string, service string, initialStatus ServiceStatus) {
g.lock.Lock()
defer g.lock.Unlock()
v := NewVertex(key, service, initialStatus)
g.Vertices[key] = v
}
// AddEdge adds a relationship of dependency between vertices `source` and `destination`
func (g *Graph) AddEdge(source string, destination string) error {
g.lock.Lock()
defer g.lock.Unlock()
sourceVertex := g.Vertices[source]
destinationVertex := g.Vertices[destination]
if sourceVertex == nil {
return fmt.Errorf("could not find %s: %w", source, api.ErrNotFound)
}
if destinationVertex == nil {
return fmt.Errorf("could not find %s: %w", destination, api.ErrNotFound)
}
// If they are already connected
if _, ok := sourceVertex.Children[destination]; ok {
return nil
}
sourceVertex.Children[destination] = destinationVertex
destinationVertex.Parents[source] = sourceVertex
return nil
}
func leaves(g *Graph) []*Vertex {
return g.Leaves()
}
// Leaves returns the slice of leaves of the graph
func (g *Graph) Leaves() []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
for _, v := range g.Vertices {
if len(v.Children) == 0 {
res = append(res, v)
}
}
return res
}
func roots(g *Graph) []*Vertex {
return g.Roots()
}
// Roots returns the slice of "Roots" of the graph
func (g *Graph) Roots() []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
for _, v := range g.Vertices {
if len(v.Parents) == 0 {
res = append(res, v)
}
}
return res
}
// UpdateStatus updates the status of a certain vertex
func (g *Graph) UpdateStatus(key string, status ServiceStatus) {
g.lock.Lock()
defer g.lock.Unlock()
g.Vertices[key].Status = status
}
func filterChildren(g *Graph, k string, s ServiceStatus) []*Vertex {
return g.FilterChildren(k, s)
}
// FilterChildren returns children of a certain vertex that are in a certain status
func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
vertex := g.Vertices[key]
for _, child := range vertex.Children {
if child.Status == status {
res = append(res, child)
}
}
return res
}
func filterParents(g *Graph, k string, s ServiceStatus) []*Vertex {
return g.FilterParents(k, s)
}
// FilterParents returns the parents of a certain vertex that are in a certain status
func (g *Graph) FilterParents(key string, status ServiceStatus) []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
vertex := g.Vertices[key]
for _, parent := range vertex.Parents {
if parent.Status == status {
res = append(res, parent)
}
}
return res
}
// HasCycles detects cycles in the graph
func (g *Graph) HasCycles() (bool, error) {
discovered := []string{}
finished := []string{}
for _, vertex := range g.Vertices {
path := []string{
vertex.Key,
}
if !slices.Contains(discovered, vertex.Key) && !slices.Contains(finished, vertex.Key) {
var err error
discovered, finished, err = g.visit(vertex.Key, path, discovered, finished)
if err != nil {
return true, err
}
}
}
return false, nil
}
func (g *Graph) visit(key string, path []string, discovered []string, finished []string) ([]string, []string, error) {
discovered = append(discovered, key)
for _, v := range g.Vertices[key].Children {
path := append(path, v.Key)
if slices.Contains(discovered, v.Key) {
return nil, nil, fmt.Errorf("cycle found: %s", strings.Join(path, " -> "))
}
if !slices.Contains(finished, v.Key) {
if _, _, err := g.visit(v.Key, path, discovered, finished); err != nil {
return nil, nil, err
}
}
}
discovered = remove(discovered, key)
finished = append(finished, key)
return discovered, finished, nil
}
func remove(slice []string, item string) []string {
var s []string
for _, i := range slice {
if i != item {
s = append(s, i)
}
}
return s
}
================================================
FILE: pkg/compose/dependencies_test.go
================================================
/*
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"
"sort"
"sync"
"testing"
"github.com/compose-spec/compose-go/v2/types"
testify "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/utils"
)
func createTestProject() *types.Project {
return &types.Project{
Services: types.Services{
"test1": {
Name: "test1",
DependsOn: map[string]types.ServiceDependency{
"test2": {},
},
},
"test2": {
Name: "test2",
DependsOn: map[string]types.ServiceDependency{
"test3": {},
},
},
"test3": {
Name: "test3",
},
},
}
}
func TestTraversalWithMultipleParents(t *testing.T) {
dependent := types.ServiceConfig{
Name: "dependent",
DependsOn: make(types.DependsOnConfig),
}
project := types.Project{
Services: types.Services{"dependent": dependent},
}
for i := 1; i <= 100; i++ {
name := fmt.Sprintf("svc_%d", i)
dependent.DependsOn[name] = types.ServiceDependency{}
svc := types.ServiceConfig{Name: name}
project.Services[name] = svc
}
svc := make(chan string, 10)
seen := make(map[string]int)
done := make(chan struct{})
go func() {
for service := range svc {
seen[service]++
}
done <- struct{}{}
}()
err := InDependencyOrder(t.Context(), &project, func(ctx context.Context, service string) error {
svc <- service
return nil
})
require.NoError(t, err, "Error during iteration")
close(svc)
<-done
testify.Len(t, seen, 101)
for svc, count := range seen {
assert.Equal(t, 1, count, "Service: %s", svc)
}
}
func TestInDependencyUpCommandOrder(t *testing.T) {
var order []string
err := InDependencyOrder(t.Context(), createTestProject(), func(ctx context.Context, service string) error {
order = append(order, service)
return nil
})
require.NoError(t, err, "Error during iteration")
require.Equal(t, []string{"test3", "test2", "test1"}, order)
}
func TestInDependencyReverseDownCommandOrder(t *testing.T) {
var order []string
err := InReverseDependencyOrder(t.Context(), createTestProject(), func(ctx context.Context, service string) error {
order = append(order, service)
return nil
})
require.NoError(t, err, "Error during iteration")
require.Equal(t, []string{"test1", "test2", "test3"}, order)
}
func TestBuildGraph(t *testing.T) {
testCases := []struct {
desc string
services types.Services
expectedVertices map[string]*Vertex
}{
{
desc: "builds graph with single service",
services: types.Services{
"test": {
Name: "test",
DependsOn: types.DependsOnConfig{},
},
},
expectedVertices: map[string]*Vertex{
"test": {
Key: "test",
Service: "test",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{},
},
},
},
{
desc: "builds graph with two separate services",
services: types.Services{
"test": {
Name: "test",
DependsOn: types.DependsOnConfig{},
},
"another": {
Name: "another",
DependsOn: types.DependsOnConfig{},
},
},
expectedVertices: map[string]*Vertex{
"test": {
Key: "test",
Service: "test",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{},
},
"another": {
Key: "another",
Service: "another",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{},
},
},
},
{
desc: "builds graph with a service and a dependency",
services: types.Services{
"test": {
Name: "test",
DependsOn: types.DependsOnConfig{
"another": types.ServiceDependency{},
},
},
"another": {
Name: "another",
DependsOn: types.DependsOnConfig{},
},
},
expectedVertices: map[string]*Vertex{
"test": {
Key: "test",
Service: "test",
Status: ServiceStopped,
Children: map[string]*Vertex{
"another": {},
},
Parents: map[string]*Vertex{},
},
"another": {
Key: "another",
Service: "another",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{
"test": {},
},
},
},
},
{
desc: "builds graph with multiple dependency levels",
services: types.Services{
"test": {
Name: "test",
DependsOn: types.DependsOnConfig{
"another": types.ServiceDependency{},
},
},
"another": {
Name: "another",
DependsOn: types.DependsOnConfig{
"another_dep": types.ServiceDependency{},
},
},
"another_dep": {
Name: "another_dep",
DependsOn: types.DependsOnConfig{},
},
},
expectedVertices: map[string]*Vertex{
"test": {
Key: "test",
Service: "test",
Status: ServiceStopped,
Children: map[string]*Vertex{
"another": {},
},
Parents: map[string]*Vertex{},
},
"another": {
Key: "another",
Service: "another",
Status: ServiceStopped,
Children: map[string]*Vertex{
"another_dep": {},
},
Parents: map[string]*Vertex{
"test": {},
},
},
"another_dep": {
Key: "another_dep",
Service: "another_dep",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{
"another": {},
},
},
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
project := types.Project{
Services: tC.services,
}
graph, err := NewGraph(&project, ServiceStopped)
assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
for k, vertex := range graph.Vertices {
expected, ok := tC.expectedVertices[k]
assert.Equal(t, true, ok)
assert.Equal(t, true, isVertexEqual(*expected, *vertex))
}
})
}
}
func TestBuildGraphDependsOn(t *testing.T) {
testCases := []struct {
desc string
services types.Services
expectedVertices map[string]*Vertex
}{
{
desc: "service depends on init container which is already removed",
services: types.Services{
"test": {
Name: "test",
DependsOn: types.DependsOnConfig{
"test-removed-init-container": types.ServiceDependency{
Condition: "service_completed_successfully",
Restart: false,
Extensions: types.Extensions(nil),
Required: false,
},
},
},
},
expectedVertices: map[string]*Vertex{
"test": {
Key: "test",
Service: "test",
Status: ServiceStopped,
Children: map[string]*Vertex{},
Parents: map[string]*Vertex{},
},
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
project := types.Project{
Services: tC.services,
}
graph, err := NewGraph(&project, ServiceStopped)
assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
for k, vertex := range graph.Vertices {
expected, ok := tC.expectedVertices[k]
assert.Equal(t, true, ok)
assert.Equal(t, true, isVertexEqual(*expected, *vertex))
}
})
}
}
func isVertexEqual(a, b Vertex) bool {
childrenEquality := true
for c := range a.Children {
if _, ok := b.Children[c]; !ok {
childrenEquality = false
}
}
parentEquality := true
for p := range a.Parents {
if _, ok := b.Parents[p]; !ok {
parentEquality = false
}
}
return a.Key == b.Key &&
a.Service == b.Service &&
childrenEquality &&
parentEquality
}
func TestWith_RootNodesAndUp(t *testing.T) {
graph := &Graph{
lock: sync.RWMutex{},
Vertices: map[string]*Vertex{},
}
/** graph topology:
A B
/ \ / \
G C E
\ /
D
|
F
*/
graph.AddVertex("A", "A", 0)
graph.AddVertex("B", "B", 0)
graph.AddVertex("C", "C", 0)
graph.AddVertex("D", "D", 0)
graph.AddVertex("E", "E", 0)
graph.AddVertex("F", "F", 0)
graph.AddVertex("G", "G", 0)
_ = graph.AddEdge("C", "A")
_ = graph.AddEdge("C", "B")
_ = graph.AddEdge("E", "B")
_ = graph.AddEdge("D", "C")
_ = graph.AddEdge("D", "E")
_ = graph.AddEdge("F", "D")
_ = graph.AddEdge("G", "A")
tests := []struct {
name string
nodes []string
want []string
}{
{
name: "whole graph",
nodes: []string{"A", "B"},
want: []string{"A", "B", "C", "D", "E", "F", "G"},
},
{
name: "only leaves",
nodes: []string{"F", "G"},
want: []string{"F", "G"},
},
{
name: "simple dependent",
nodes: []string{"D"},
want: []string{"D", "F"},
},
{
name: "diamond dependents",
nodes: []string{"B"},
want: []string{"B", "C", "D", "E", "F"},
},
{
name: "partial graph",
nodes: []string{"A"},
want: []string{"A", "C", "D", "F", "G"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mx := sync.Mutex{}
expected := utils.Set[string]{}
expected.AddAll("C", "G", "D", "F")
var visited []string
gt := downDirectionTraversal(func(ctx context.Context, s string) error {
mx.Lock()
defer mx.Unlock()
visited = append(visited, s)
return nil
})
WithRootNodesAndDown(tt.nodes)(gt)
err := gt.visit(t.Context(), graph)
assert.NilError(t, err)
sort.Strings(visited)
assert.DeepEqual(t, tt.want, visited)
})
}
}
================================================
FILE: pkg/compose/desktop.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"strings"
"github.com/moby/moby/client"
)
// engineLabelDesktopAddress is used to detect that Compose is running with a
// Docker Desktop context. When this label is present, the value is an endpoint
// address for an in-memory socket (AF_UNIX or named pipe).
const engineLabelDesktopAddress = "com.docker.desktop.address"
func (s *composeService) isDesktopIntegrationActive(ctx context.Context) (bool, error) {
res, err := s.apiClient().Info(ctx, client.InfoOptions{})
if err != nil {
return false, err
}
for _, l := range res.Info.Labels {
k, _, ok := strings.Cut(l, "=")
if ok && k == engineLabelDesktopAddress {
return true, nil
}
}
return false, nil
}
================================================
FILE: pkg/compose/docker_cli_providers.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"github.com/docker/cli/cli/command"
)
// dockerCliContextInfo implements api.ContextInfo using Docker CLI
type dockerCliContextInfo struct {
cli command.Cli
}
func (c *dockerCliContextInfo) CurrentContext() string {
return c.cli.CurrentContext()
}
func (c *dockerCliContextInfo) ServerOSType() string {
return c.cli.ServerInfo().OSType
}
func (c *dockerCliContextInfo) BuildKitEnabled() (bool, error) {
return c.cli.BuildKitEnabled()
}
================================================
FILE: pkg/compose/down.go
================================================
/*
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"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
containerType "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
type downOp func() error
func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.down(ctx, strings.ToLower(projectName), options)
}, "down", s.events)
}
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { //nolint:gocyclo
resourceToRemove := false
include := oneOffExclude
if options.RemoveOrphans {
include = oneOffInclude
}
containers, err := s.getContainers(ctx, projectName, include, true)
if err != nil {
return err
}
project := options.Project
if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil {
return err
}
}
// Check requested services exists in model
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 {
serv := project.Services[service]
if serv.Provider != nil {
return s.runPlugin(ctx, project, serv, "down")
}
serviceContainers := containers.filter(isService(service))
err := s.removeContainers(ctx, serviceContainers, &serv, options.Timeout, options.Volumes)
return err
}, WithRootNodesAndDown(options.Services))
if err != nil {
return err
}
orphans := containers.filter(isOrphaned(project))
if options.RemoveOrphans && len(orphans) > 0 {
err := s.removeContainers(ctx, orphans, nil, options.Timeout, false)
if err != nil {
return err
}
}
ops := s.ensureNetworksDown(ctx, project)
if options.Images != "" {
imgOps, err := s.ensureImagesDown(ctx, project, options)
if err != nil {
return err
}
ops = append(ops, imgOps...)
}
if options.Volumes {
ops = append(ops, s.ensureVolumesDown(ctx, project)...)
}
if !resourceToRemove && len(ops) == 0 {
logrus.Warnf("Warning: No resource found to remove for project %q.", projectName)
}
eg, ctx := errgroup.WithContext(ctx)
for _, op := range ops {
eg.Go(op)
}
return eg.Wait()
}
func checkSelectedServices(options api.DownOptions, project *types.Project) ([]string, error) {
var services []string
for _, service := range options.Services {
_, err := project.GetService(service)
if err != nil {
if options.Project != nil {
// ran with an explicit compose.yaml file, so we should not ignore
return nil, err
}
// ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed
} else {
services = append(services, service)
}
}
return services, nil
}
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project) []downOp {
var ops []downOp
for _, vol := range project.Volumes {
if vol.External {
continue
}
volumeName := vol.Name
ops = append(ops, func() error {
return s.removeVolume(ctx, volumeName)
})
}
return ops
}
func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions) ([]downOp, error) {
imagePruner := NewImagePruner(s.apiClient(), project)
pruneOpts := ImagePruneOptions{
Mode: ImagePruneMode(options.Images),
RemoveOrphans: options.RemoveOrphans,
}
images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
if err != nil {
return nil, err
}
var ops []downOp
for i := range images {
img := images[i]
ops = append(ops, func() error {
return s.removeImage(ctx, img)
})
}
return ops, nil
}
func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project) []downOp {
var ops []downOp
for key, n := range project.Networks {
if n.External {
continue
}
// loop capture variable for op closure
networkKey := key
idOrName := n.Name
ops = append(ops, func() error {
return s.removeNetwork(ctx, networkKey, project.Name, idOrName)
})
}
return ops
}
func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string) error {
res, err := s.apiClient().NetworkList(ctx, client.NetworkListOptions{
Filters: projectFilter(projectName).Add("label", networkFilter(composeNetworkName)),
})
if err != nil {
return fmt.Errorf("failed to list networks: %w", err)
}
networks := res.Items
if len(networks) == 0 {
return nil
}
eventName := fmt.Sprintf("Network %s", name)
s.events.On(removingEvent(eventName))
var found int
for _, net := range networks {
if net.Name != name {
continue
}
nwInspect, err := s.apiClient().NetworkInspect(ctx, net.ID, client.NetworkInspectOptions{})
if errdefs.IsNotFound(err) {
s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
return nil
}
if err != nil {
return err
}
nw := nwInspect.Network
if len(nw.Containers) > 0 {
s.events.On(newEvent(eventName, api.Warning, "Resource is still in use"))
found++
continue
}
if _, err := s.apiClient().NetworkRemove(ctx, net.ID, client.NetworkRemoveOptions{}); err != nil {
if errdefs.IsNotFound(err) {
continue
}
s.events.On(errorEvent(eventName, err.Error()))
return fmt.Errorf("failed to remove network %s: %w", name, err)
}
s.events.On(removedEvent(eventName))
found++
}
if found == 0 {
// in practice, it's extremely unlikely for this to ever occur, as it'd
// mean the network was present when we queried at the start of this
// method but was then deleted by something else in the interim
s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
return nil
}
return nil
}
func (s *composeService) removeImage(ctx context.Context, image string) error {
id := fmt.Sprintf("Image %s", image)
s.events.On(newEvent(id, api.Working, "Removing"))
_, err := s.apiClient().ImageRemove(ctx, image, client.ImageRemoveOptions{})
if err == nil {
s.events.On(newEvent(id, api.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
s.events.On(newEvent(id, api.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
s.events.On(newEvent(id, api.Done, "Warning: No resource found to remove"))
return nil
}
return err
}
func (s *composeService) removeVolume(ctx context.Context, id string) error {
resource := fmt.Sprintf("Volume %s", id)
_, err := s.apiClient().VolumeInspect(ctx, id, client.VolumeInspectOptions{})
if errdefs.IsNotFound(err) {
// Already gone
return nil
}
s.events.On(newEvent(resource, api.Working, "Removing"))
_, err = s.apiClient().VolumeRemove(ctx, id, client.VolumeRemoveOptions{
Force: true,
})
if err == nil {
s.events.On(newEvent(resource, api.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
s.events.On(newEvent(resource, api.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
s.events.On(newEvent(resource, api.Done, "Warning: No resource found to remove"))
return nil
}
return err
}
func (s *composeService) stopContainer(ctx context.Context, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error {
eventName := getContainerProgressName(ctr)
s.events.On(stoppingEvent(eventName))
if service != nil {
for _, hook := range service.PreStop {
err := s.runHook(ctx, ctr, *service, hook, listener)
if err != nil {
// Ignore errors indicating that some containers were already stopped or removed.
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
return nil
}
return err
}
}
}
_, err := s.apiClient().ContainerStop(ctx, ctr.ID, client.ContainerStopOptions{
Timeout: utils.DurationSecondToInt(timeout),
})
if err != nil {
s.events.On(errorEvent(eventName, "Error while Stopping"))
return err
}
s.events.On(stoppedEvent(eventName))
return nil
}
func (s *composeService) stopContainers(ctx context.Context, serv *types.ServiceConfig, containers []containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error {
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
return s.stopContainer(ctx, serv, ctr, timeout, listener)
})
}
return eg.Wait()
}
func (s *composeService) removeContainers(ctx context.Context, containers []containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
return s.stopAndRemoveContainer(ctx, ctr, service, timeout, volumes)
})
}
return eg.Wait()
}
func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
eventName := getContainerProgressName(ctr)
err := s.stopContainer(ctx, service, ctr, timeout, nil)
if errdefs.IsNotFound(err) {
s.events.On(removedEvent(eventName))
return nil
}
if err != nil {
return err
}
s.events.On(removingEvent(eventName))
_, err = s.apiClient().ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{
Force: true,
RemoveVolumes: volumes,
})
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
s.events.On(errorEvent(eventName, "Error while Removing"))
return err
}
s.events.On(removedEvent(eventName))
return nil
}
func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
containers = containers.filter(isNotOneOff)
p, err := s.projectFromName(containers, projectName)
if err != nil && !api.IsNotFoundError(err) {
return nil, err
}
project, err := p.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
for k := range service.DependsOn {
if dependency, ok := service.DependsOn[k]; ok {
dependency.Required = false
service.DependsOn[k] = dependency
}
}
return service, nil
})
if err != nil {
return nil, err
}
volumes, err := s.actualVolumes(ctx, projectName)
if err != nil {
return nil, err
}
project.Volumes = volumes
networks, err := s.actualNetworks(ctx, projectName)
if err != nil {
return nil, err
}
project.Networks = networks
return project, nil
}
================================================
FILE: pkg/compose/down_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"os"
"strings"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestDown(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
client.ContainerListResult{Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
}}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{Items: []network.Summary{
{Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
{Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
}}, nil)
stopOptions := client.ContainerStopOptions{}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "456", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "456", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "789", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
}).Return(client.NetworkListResult{Items: []network.Summary{
{Network: network.Network{ID: "abc123", Name: "myProject_default"}},
{Network: network.Network{ID: "def456", Name: "myProject_default"}},
}}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{
Network: network.Inspect{Network: network.Network{ID: "abc123"}},
}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "def456", gomock.Any()).Return(client.NetworkInspectResult{
Network: network.Inspect{Network: network.Network{ID: "def456"}},
}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "def456", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{})
assert.NilError(t, err)
}
func TestDownWithGivenServices(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{Items: []network.Summary{
{Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
{Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
}}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
}).Return(client.NetworkListResult{Items: []network.Summary{
{Network: network.Network{ID: "abc123", Name: "myProject_default"}},
}}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{Network: network.Inspect{Network: network.Network{ID: "abc123"}}}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
err = tested.Down(t.Context(), 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, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{Items: []network.Summary{
{Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
{Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
}}, nil)
err = tested.Down(t.Context(), 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()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return(
client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{
Items: []network.Summary{{
Network: network.Network{
Name: "myProject_default",
Labels: map[string]string{compose.NetworkLabel: "default"},
},
}},
}, nil)
stopOptions := client.ContainerStopOptions{}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "321", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "789", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "321", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
}).Return(client.NetworkListResult{
Items: []network.Summary{{Network: network.Network{ID: "abc123", Name: "myProject_default"}}},
}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{
Network: network.Inspect{Network: network.Network{ID: "abc123"}},
}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
assert.NilError(t, err)
}
func TestDownRemoveVolumes(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
client.ContainerListResult{
Items: []container.Summary{testContainer("service1", "123", false)},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{
Items: []volume.Volume{{Name: "myProject_volume"}},
}, nil)
api.EXPECT().VolumeInspect(gomock.Any(), "myProject_volume", gomock.Any()).
Return(client.VolumeInspectResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", client.VolumeRemoveOptions{Force: true}).Return(client.VolumeRemoveResult{}, nil)
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
assert.NilError(t, err)
}
func TestDownRemoveImages(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
opts := compose.DownOptions{
Project: &types.Project{
Name: strings.ToLower(testProject),
Services: types.Services{
"local-anonymous": {Name: "local-anonymous"},
"local-named": {Name: "local-named", Image: "local-named-image"},
"remote": {Name: "remote", Image: "remote-image"},
"remote-tagged": {Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"},
"no-images-anonymous": {Name: "no-images-anonymous"},
"no-images-named": {Name: "no-images-named", Image: "missing-named-image"},
},
},
}
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).
Return(client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
},
}, nil).
AnyTimes()
api.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("dangling", "false"),
}).Return(client.ImageListResult{Items: []image.Summary{
{
Labels: types.Labels{compose.ServiceLabel: "local-anonymous"},
RepoTags: []string{"testproject-local-anonymous:latest"},
},
{
Labels: types.Labels{compose.ServiceLabel: "local-named"},
RepoTags: []string{"local-named-image:latest"},
},
}}, nil).AnyTimes()
imagesToBeInspected := map[string]bool{
"testproject-local-anonymous": true,
"local-named-image": true,
"remote-image": true,
"testproject-no-images-anonymous": false,
"missing-named-image": false,
}
for img, exists := range imagesToBeInspected {
var resp image.InspectResponse
var err error
if exists {
resp.RepoTags = []string{img}
} else {
err = errdefs.ErrNotFound.WithMessage(fmt.Sprintf("test specified that image %q should not exist", img))
}
api.EXPECT().ImageInspect(gomock.Any(), img).
Return(client.ImageInspectResult{InspectResponse: resp}, err).
AnyTimes()
}
api.EXPECT().ImageInspect(gomock.Any(), "registry.example.com/remote-image-tagged:v1.0").
Return(client.ImageInspectResult{InspectResponse: image.InspectResponse{RepoTags: []string{"registry.example.com/remote-image-tagged:v1.0"}}}, nil).
AnyTimes()
localImagesToBeRemoved := []string{
"testproject-local-anonymous:latest",
"local-named-image:latest",
}
for _, img := range localImagesToBeRemoved {
// test calls down --rmi=local then down --rmi=all, so local images
// get "removed" 2x, while other images are only 1x
api.EXPECT().ImageRemove(gomock.Any(), img, client.ImageRemoveOptions{}).
Return(client.ImageRemoveResult{}, nil).
Times(2)
}
t.Log("-> docker compose down --rmi=local")
opts.Images = "local"
err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
assert.NilError(t, err)
otherImagesToBeRemoved := []string{
"remote-image:latest",
"registry.example.com/remote-image-tagged:v1.0",
}
for _, img := range otherImagesToBeRemoved {
api.EXPECT().ImageRemove(gomock.Any(), img, client.ImageRemoveOptions{}).
Return(client.ImageRemoveResult{}, nil).
Times(1)
}
t.Log("-> docker compose down --rmi=all")
opts.Images = "all"
err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
assert.NilError(t, err)
}
func TestDownRemoveImages_NoLabel(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
ctr := testContainer("service1", "123", false)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
client.ContainerListResult{
Items: []container.Summary{ctr},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{
Items: []volume.Volume{{Name: "myProject_volume"}},
}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{}, nil)
// ImageList returns no images for the project since they were unlabeled
// (created by an older version of Compose)
api.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("dangling", "false"),
}).Return(client.ImageListResult{}, nil)
api.EXPECT().ImageInspect(gomock.Any(), "testproject-service1", gomock.Any()).Return(client.ImageInspectResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ImageRemove(gomock.Any(), "testproject-service1:latest", client.ImageRemoveOptions{}).Return(client.ImageRemoveResult{}, nil)
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
assert.NilError(t, err)
}
func prepareMocks(mockCtrl *gomock.Controller) (*mocks.MockAPIClient, *mocks.MockCli) {
api := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
cli.EXPECT().Client().Return(api).AnyTimes()
cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
cli.EXPECT().Out().Return(streams.NewOut(os.Stdout)).AnyTimes()
return api, cli
}
================================================
FILE: pkg/compose/envresolver.go
================================================
/*
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 (
"runtime"
"strings"
)
// isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively.
var isCaseInsensitiveEnvVars = (runtime.GOOS == "windows")
// envResolver returns resolver for environment variables suitable for the current platform.
// Expected to be used with `MappingWithEquals.Resolve`.
// Updates in `environment` may not be reflected.
func envResolver(environment map[string]string) func(string) (string, bool) {
return envResolverWithCase(environment, isCaseInsensitiveEnvVars)
}
// envResolverWithCase returns resolver for environment variables with the specified case-sensitive condition.
// Expected to be used with `MappingWithEquals.Resolve`.
// Updates in `environment` may not be reflected.
func envResolverWithCase(environment map[string]string, caseInsensitive bool) func(string) (string, bool) {
if environment == nil {
return func(s string) (string, bool) {
return "", false
}
}
if !caseInsensitive {
return func(s string) (string, bool) {
v, ok := environment[s]
return v, ok
}
}
// variable names must be treated case-insensitively.
// Resolves in this way:
// * Return the value if its name matches with the passed name case-sensitively.
// * Otherwise, return the value if its lower-cased name matches lower-cased passed name.
// * The value is indefinite if multiple variable matches.
loweredEnvironment := make(map[string]string, len(environment))
for k, v := range environment {
loweredEnvironment[strings.ToLower(k)] = v
}
return func(s string) (string, bool) {
v, ok := environment[s]
if ok {
return v, ok
}
v, ok = loweredEnvironment[strings.ToLower(s)]
return v, ok
}
}
================================================
FILE: pkg/compose/envresolver_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"gotest.tools/v3/assert"
)
func Test_EnvResolverWithCase(t *testing.T) {
tests := []struct {
name string
environment map[string]string
caseInsensitive bool
search string
expectedValue string
expectedOk bool
}{
{
name: "case sensitive/case match",
environment: map[string]string{
"Env1": "Value1",
"Env2": "Value2",
},
caseInsensitive: false,
search: "Env1",
expectedValue: "Value1",
expectedOk: true,
},
{
name: "case sensitive/case unmatch",
environment: map[string]string{
"Env1": "Value1",
"Env2": "Value2",
},
caseInsensitive: false,
search: "ENV1",
expectedValue: "",
expectedOk: false,
},
{
name: "case sensitive/nil environment",
environment: nil,
caseInsensitive: false,
search: "Env1",
expectedValue: "",
expectedOk: false,
},
{
name: "case insensitive/case match",
environment: map[string]string{
"Env1": "Value1",
"Env2": "Value2",
},
caseInsensitive: true,
search: "Env1",
expectedValue: "Value1",
expectedOk: true,
},
{
name: "case insensitive/case unmatch",
environment: map[string]string{
"Env1": "Value1",
"Env2": "Value2",
},
caseInsensitive: true,
search: "ENV1",
expectedValue: "Value1",
expectedOk: true,
},
{
name: "case insensitive/unmatch",
environment: map[string]string{
"Env1": "Value1",
"Env2": "Value2",
},
caseInsensitive: true,
search: "Env3",
expectedValue: "",
expectedOk: false,
},
{
name: "case insensitive/nil environment",
environment: nil,
caseInsensitive: true,
search: "Env1",
expectedValue: "",
expectedOk: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := envResolverWithCase(test.environment, test.caseInsensitive)
v, ok := f(test.search)
assert.Equal(t, v, test.expectedValue)
assert.Equal(t, ok, test.expectedOk)
})
}
}
================================================
FILE: pkg/compose/events.go
================================================
/*
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"
"slices"
"strings"
"time"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
projectName = strings.ToLower(projectName)
res := s.apiClient().Events(ctx, client.EventsListOptions{
Filters: projectFilter(projectName),
Since: options.Since,
Until: options.Until,
})
for {
select {
case event := <-res.Messages:
// TODO: support other event types
if event.Type != "container" {
continue
}
if event.Actor.Attributes[api.OneoffLabel] == "True" {
// ignore
continue
}
service := event.Actor.Attributes[api.ServiceLabel]
if len(options.Services) > 0 && !slices.Contains(options.Services, service) {
continue
}
attributes := map[string]string{}
for k, v := range event.Actor.Attributes {
if strings.HasPrefix(k, "com.docker.compose.") {
continue
}
attributes[k] = v
}
timestamp := time.Unix(event.Time, 0)
if event.TimeNano != 0 {
timestamp = time.Unix(0, event.TimeNano)
}
err := options.Consumer(api.Event{
Timestamp: timestamp,
Service: service,
Container: event.Actor.ID,
Status: string(event.Action),
Attributes: attributes,
})
if err != nil {
return err
}
case err := <-res.Err:
return err
}
}
}
================================================
FILE: pkg/compose/exec.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command/container"
containerType "github.com/moby/moby/api/types/container"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) {
projectName = strings.ToLower(projectName)
target, err := s.getExecTarget(ctx, projectName, options)
if err != nil {
return 0, err
}
exec := container.NewExecOptions()
exec.Interactive = options.Interactive
exec.TTY = options.Tty
exec.Detach = options.Detach
exec.User = options.User
exec.Privileged = options.Privileged
exec.Workdir = options.WorkingDir
exec.Command = options.Command
for _, v := range options.Environment {
err := exec.Env.Set(v)
if err != nil {
return 0, err
}
}
err = container.RunExec(ctx, s.dockerCli, target.ID, exec)
var sterr cli.StatusError
if errors.As(err, &sterr) {
return sterr.StatusCode, err
}
return 0, err
}
func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (containerType.Summary, error) {
return s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, opts.Service, opts.Index)
}
================================================
FILE: pkg/compose/export.go
================================================
/*
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/moby/moby/client"
"github.com/moby/sys/atomicwriter"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.export(ctx, projectName, options)
}, "export", s.events)
}
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 == "" {
if s.stdout().IsTerminal() {
return fmt.Errorf("output option is required when exporting to terminal")
}
} else if err := command.ValidateOutputPath(options.Output); err != nil {
return fmt.Errorf("failed to export container: %w", err)
}
name := getCanonicalContainerName(container)
s.events.On(api.Resource{
ID: name,
Text: api.StatusExporting,
Status: api.Working,
})
responseBody, err := s.apiClient().ContainerExport(ctx, container.ID, client.ContainerExportOptions{})
if err != nil {
return err
}
defer func() {
if err := responseBody.Close(); err != nil {
s.events.On(errorEventf(name, "Failed to close response body: %s", err.Error()))
}
}()
if !s.dryRun {
if options.Output == "" {
_, err := io.Copy(s.stdout(), responseBody)
return err
} else {
writer, err := atomicwriter.New(options.Output, 0o600)
if err != nil {
return err
}
defer func() { _ = writer.Close() }()
_, err = io.Copy(writer, responseBody)
return err
}
}
s.events.On(api.Resource{
ID: name,
Text: api.StatusExported,
Status: api.Done,
})
return nil
}
================================================
FILE: pkg/compose/filters.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func projectFilter(projectName string) client.Filters {
return make(client.Filters).Add("label", fmt.Sprintf("%s=%s", api.ProjectLabel, projectName))
}
func serviceFilter(serviceName string) string {
return fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName)
}
func networkFilter(name string) string {
return fmt.Sprintf("%s=%s", api.NetworkLabel, name)
}
func oneOffFilter(b bool) string {
v := "False"
if b {
v = "True"
}
return fmt.Sprintf("%s=%s", api.OneoffLabel, v)
}
func containerNumberFilter(index int) string {
return fmt.Sprintf("%s=%d", api.ContainerNumberLabel, index)
}
func hasConfigHashLabel() string {
return api.ConfigHashLabel
}
================================================
FILE: pkg/compose/generate.go
================================================
/*
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"
"maps"
"slices"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: make(client.Filters).Add("name", options.Containers...),
All: true,
})
if err != nil {
return nil, err
}
containers := res.Items
containersByIds, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: make(client.Filters).Add("id", options.Containers...),
All: true,
})
if err != nil {
return nil, err
}
for _, ctr := range containersByIds.Items {
if !slices.ContainsFunc(containers, func(summary container.Summary) bool {
return summary.ID == ctr.ID
}) {
containers = append(containers, ctr)
}
}
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 []container.Summary, 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, client.ContainerInspectOptions{})
if err != nil {
services[serviceLabel] = service
continue
}
s.extractComposeConfiguration(&service, inspect.Container, 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 container.InspectResponse, 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.Num()),
Published: portBinding.HostPort,
Protocol: string(key.Proto()),
HostIP: portBinding.HostIP.String(),
})
}
}
}
}
func (s *composeService) toComposeHealthCheck(healthConfig *container.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 []container.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, client.NetworkInspectOptions{})
if err != nil {
networkConfigs[name] = types.NetworkConfig{}
} else {
networkConfigs[name] = types.NetworkConfig{
Internal: inspect.Network.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
}
================================================
FILE: pkg/compose/hash.go
================================================
/*
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 (
"encoding/json"
"github.com/compose-spec/compose-go/v2/types"
"github.com/opencontainers/go-digest"
)
// ServiceHash computes the configuration hash for a service.
func ServiceHash(o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil
o.PullPolicy = ""
o.Scale = nil
if o.Deploy != nil {
o.Deploy.Replicas = nil
}
o.DependsOn = nil
o.Profiles = nil
bytes, err := json.Marshal(o)
if err != nil {
return "", err
}
return digest.SHA256.FromBytes(bytes).Encoded(), nil
}
// NetworkHash computes the configuration hash for a network.
func NetworkHash(o *types.NetworkConfig) (string, error) {
bytes, err := json.Marshal(o)
if err != nil {
return "", err
}
return digest.SHA256.FromBytes(bytes).Encoded(), nil
}
// VolumeHash computes the configuration hash for a volume.
func VolumeHash(o types.VolumeConfig) (string, error) {
if o.Driver == "" { // (TODO: jhrotko) This probably should be fixed in compose-go
o.Driver = "local"
}
bytes, err := json.Marshal(o)
if err != nil {
return "", err
}
return digest.SHA256.FromBytes(bytes).Encoded(), nil
}
================================================
FILE: pkg/compose/hash_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestServiceHash(t *testing.T) {
hash1, err := ServiceHash(serviceConfig(1))
assert.NilError(t, err)
hash2, err := ServiceHash(serviceConfig(2))
assert.NilError(t, err)
assert.Equal(t, hash1, hash2)
}
func serviceConfig(replicas int) types.ServiceConfig {
return types.ServiceConfig{
Scale: &replicas,
Deploy: &types.DeployConfig{
Replicas: &replicas,
},
Name: "foo",
Image: "bar",
}
}
================================================
FILE: pkg/compose/hook.go
================================================
/*
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/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func (s composeService) runHook(ctx context.Context, ctr container.Summary, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error {
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.HookEventLog,
Source: getContainerNameWithoutProject(ctr) + " ->",
ID: ctr.ID,
Service: service.Name,
Line: line,
})
})
defer wOut.Close() //nolint:errcheck
detached := listener == nil
exec, err := s.apiClient().ExecCreate(ctx, ctr.ID, client.ExecCreateOptions{
User: hook.User,
Privileged: hook.Privileged,
Env: ToMobyEnv(hook.Environment),
WorkingDir: hook.WorkingDir,
Cmd: hook.Command,
AttachStdout: !detached,
AttachStderr: !detached,
})
if err != nil {
return err
}
if detached {
return s.runWaitExec(ctx, exec.ID, service, listener)
}
attachOptions := client.ExecAttachOptions{
TTY: service.Tty,
}
if service.Tty {
height, width := s.stdout().GetTtySize()
attachOptions.ConsoleSize = client.ConsoleSize{
Width: width,
Height: height,
}
}
attach, err := s.apiClient().ExecAttach(ctx, exec.ID, attachOptions)
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().ExecInspect(ctx, exec.ID, client.ExecInspectOptions{})
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, execID string, service types.ServiceConfig, listener api.ContainerEventListener) error {
_, err := s.apiClient().ExecStart(ctx, execID, client.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().ExecInspect(ctx, execID, client.ExecInspectOptions{})
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
}
}
}
}
================================================
FILE: pkg/compose/hook_test.go
================================================
/*
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 (
"net"
"os"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/console"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)
// TestRunHook_ConsoleSize verifies that ConsoleSize is only passed to ExecAttach
// when the service has TTY enabled. When TTY is disabled, passing a non-zero
// ConsoleSize causes the Docker daemon to return "console size is only supported
// when TTY is enabled" (regression introduced in v5.1.0).
func TestRunHook_ConsoleSize(t *testing.T) {
tests := []struct {
name string
tty bool
expectedConsole client.ConsoleSize
}{
{
name: "no tty - ConsoleSize must be zero",
tty: false,
expectedConsole: client.ConsoleSize{},
},
{
name: "with tty - ConsoleSize should reflect terminal dimensions",
tty: true,
expectedConsole: client.ConsoleSize{Width: 80, Height: 24},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAPI := mocks.NewMockAPIClient(mockCtrl)
mockCli := mocks.NewMockCli(mockCtrl)
mockCli.EXPECT().Client().Return(mockAPI).AnyTimes()
mockCli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
// Create a PTY so GetTtySize() returns real non-zero dimensions,
// simulating an interactive terminal session.
pty, slavePath, err := console.NewPty()
assert.NilError(t, err)
defer pty.Close() //nolint:errcheck
assert.NilError(t, pty.Resize(console.WinSize{Height: 24, Width: 80}))
slaveFile, err := os.OpenFile(slavePath, os.O_RDWR, 0)
assert.NilError(t, err)
defer slaveFile.Close() //nolint:errcheck
mockCli.EXPECT().Out().Return(streams.NewOut(slaveFile)).AnyTimes()
service := types.ServiceConfig{
Name: "test",
Tty: tc.tty,
}
hook := types.ServiceHook{Command: []string{"echo", "hello"}}
ctr := container.Summary{ID: "container123"}
mockAPI.EXPECT().
ExecCreate(gomock.Any(), "container123", gomock.Any()).
Return(client.ExecCreateResult{ID: "exec123"}, nil)
// Return a pipe that immediately closes so the reader gets EOF.
serverConn, clientConn := net.Pipe()
serverConn.Close() //nolint:errcheck
mockAPI.EXPECT().
ExecAttach(gomock.Any(), "exec123", client.ExecAttachOptions{
TTY: tc.tty,
ConsoleSize: tc.expectedConsole,
}).
Return(client.ExecAttachResult{
HijackedResponse: client.NewHijackedResponse(clientConn, ""),
}, nil)
mockAPI.EXPECT().
ExecInspect(gomock.Any(), "exec123", gomock.Any()).
Return(client.ExecInspectResult{ExitCode: 0}, nil)
s, err := NewComposeService(mockCli)
assert.NilError(t, err)
noopListener := func(api.ContainerEvent) {}
err = s.(*composeService).runHook(t.Context(), ctr, service, hook, noopListener)
assert.NilError(t, err)
})
}
}
================================================
FILE: pkg/compose/image_pruner.go
================================================
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"sort"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
// ImagePruneMode controls how aggressively images associated with the project
// are removed from the engine.
type ImagePruneMode string
const (
// ImagePruneNone indicates that no project images should be removed.
ImagePruneNone ImagePruneMode = ""
// ImagePruneLocal indicates that only images built locally by Compose
// should be removed.
ImagePruneLocal ImagePruneMode = "local"
// ImagePruneAll indicates that all project-associated images, including
// remote images should be removed.
ImagePruneAll ImagePruneMode = "all"
)
// ImagePruneOptions controls the behavior of image pruning.
type ImagePruneOptions struct {
Mode ImagePruneMode
// RemoveOrphans will result in the removal of images that were built for
// the project regardless of whether they are for a known service if true.
RemoveOrphans bool
}
// ImagePruner handles image removal during Compose `down` operations.
type ImagePruner struct {
client client.ImageAPIClient
project *types.Project
}
// NewImagePruner creates an ImagePruner object for a project.
func NewImagePruner(imageClient client.ImageAPIClient, project *types.Project) *ImagePruner {
return &ImagePruner{
client: imageClient,
project: project,
}
}
// ImagesToPrune returns the set of images that should be removed.
func (p *ImagePruner) ImagesToPrune(ctx context.Context, opts ImagePruneOptions) ([]string, error) {
if opts.Mode == ImagePruneNone {
return nil, nil
} else if opts.Mode != ImagePruneLocal && opts.Mode != ImagePruneAll {
return nil, fmt.Errorf("unsupported image prune mode: %s", opts.Mode)
}
var images []string
if opts.Mode == ImagePruneAll {
namedImages, err := p.namedImages(ctx)
if err != nil {
return nil, err
}
images = append(images, namedImages...)
}
projectImages, err := p.labeledLocalImages(ctx)
if err != nil {
return nil, err
}
for _, img := range projectImages {
if len(img.RepoTags) == 0 {
// currently, we're only pruning the tagged references, but
// if we start removing the dangling images and grouping by
// service, we can remove this (and should rely on `Image::ID`)
continue
}
var shouldPrune bool
if opts.RemoveOrphans {
// indiscriminately prune all project images even if they're not
// referenced by the current Compose state (e.g. the service was
// removed from YAML)
shouldPrune = true
} else {
// only prune the image if it belongs to a known service for the project.
if _, err := p.project.GetService(img.Labels[api.ServiceLabel]); err == nil {
shouldPrune = true
}
}
if shouldPrune {
images = append(images, img.RepoTags[0])
}
}
fallbackImages, err := p.unlabeledLocalImages(ctx)
if err != nil {
return nil, err
}
images = append(images, fallbackImages...)
images = normalizeAndDedupeImages(images)
return images, nil
}
// namedImages are those that are explicitly named in the service config.
//
// These could be registry-only images (no local build), hybrid (support build
// as a fallback if cannot pull), or local-only (image does not exist in a
// registry).
func (p *ImagePruner) namedImages(ctx context.Context) ([]string, error) {
var images []string
for _, service := range p.project.Services {
if service.Image == "" {
continue
}
images = append(images, service.Image)
}
return p.filterImagesByExistence(ctx, images)
}
// labeledLocalImages are images that were locally-built by a current version of
// Compose (it did not always label built images).
//
// The image name could either have been defined by the user or implicitly
// created from the project + service name.
func (p *ImagePruner) labeledLocalImages(ctx context.Context) ([]image.Summary, error) {
res, err := p.client.ImageList(ctx, client.ImageListOptions{
// TODO(milas): we should really clean up the dangling images as
// well (historically we have NOT); need to refactor this to handle
// it gracefully without producing confusing CLI output, i.e. we
// do not want to print out a bunch of untagged/dangling image IDs,
// they should be grouped into a logical operation for the relevant
// service
Filters: projectFilter(p.project.Name).Add("dangling", "false"),
})
if err != nil {
return nil, err
}
return res.Items, nil
}
// unlabeledLocalImages are images that match the implicit naming convention
// for locally-built images but did not get labeled, presumably because they
// were produced by an older version of Compose.
//
// This is transitional to ensure `down` continues to work as expected on
// projects built/launched by previous versions of Compose. It can safely
// be removed after some time.
func (p *ImagePruner) unlabeledLocalImages(ctx context.Context) ([]string, error) {
var images []string
for _, service := range p.project.Services {
if service.Image != "" {
continue
}
img := api.GetImageNameOrDefault(service, p.project.Name)
images = append(images, img)
}
return p.filterImagesByExistence(ctx, images)
}
// filterImagesByExistence returns the subset of images that exist in the
// engine store.
//
// NOTE: Any transient errors communicating with the API will result in an
// image being returned as "existing", as this method is exclusively used to
// find images to remove, so the worst case of being conservative here is an
// attempt to remove an image that doesn't exist, which will cause a warning
// but is otherwise harmless.
func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []string) ([]string, error) {
var mu sync.Mutex
var ret []string
eg, ctx := errgroup.WithContext(ctx)
for _, img := range imageNames {
eg.Go(func() error {
_, err := p.client.ImageInspect(ctx, img)
if errdefs.IsNotFound(err) {
// err on the side of caution: only skip if we successfully
// queried the API and got back a definitive "not exists"
return nil
}
mu.Lock()
defer mu.Unlock()
ret = append(ret, img)
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return ret, nil
}
// normalizeAndDedupeImages returns the unique set of images after normalization.
func normalizeAndDedupeImages(images []string) []string {
seen := make(map[string]struct{}, len(images))
for _, img := range images {
// since some references come from user input (service.image) and some
// come from the engine API, we standardize them, opting for the
// familiar name format since they'll also be displayed in the CLI
ref, err := reference.ParseNormalizedNamed(img)
if err == nil {
ref = reference.TagNameOnly(ref)
img = reference.FamiliarString(ref)
}
seen[img] = struct{}{}
}
ret := make([]string, 0, len(seen))
for v := range seen {
ret = append(ret, v)
}
// ensure a deterministic return result - the actual ordering is not useful
sort.Strings(ret)
return ret
}
================================================
FILE: pkg/compose/images.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/versions"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
projectName = strings.ToLower(projectName)
allContainers, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: projectFilter(projectName),
})
if err != nil {
return nil, err
}
var containers []container.Summary
if len(options.Services) > 0 {
// filter service containers
for _, c := range allContainers.Items {
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
containers = append(containers, c)
}
}
} else {
containers = allContainers.Items
}
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, err
}
withPlatform := versions.GreaterThanOrEqualTo(version, apiVersion149)
summary := map[string]api.ImageSummary{}
var mux sync.Mutex
eg, ctx := errgroup.WithContext(ctx)
for _, c := range containers {
eg.Go(func() error {
image, err := s.apiClient().ImageInspect(ctx, c.Image)
if err != nil {
return err
}
id := image.ID // platform-specific image ID can't be combined with image tag, see https://github.com/moby/moby/issues/49995
if withPlatform && c.ImageManifestDescriptor != nil && c.ImageManifestDescriptor.Platform != nil {
image, err = s.apiClient().ImageInspect(ctx, c.Image, client.ImageInspectWithPlatform(c.ImageManifestDescriptor.Platform))
if err != nil {
return err
}
}
var repository, tag string
ref, err := reference.ParseDockerRef(c.Image)
if err == nil {
// ParseDockerRef will reject a local image ID
repository = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
var created *time.Time
if image.Created != "" {
t, err := time.Parse(time.RFC3339Nano, image.Created)
if err != nil {
return err
}
created = &t
}
mux.Lock()
defer mux.Unlock()
summary[getCanonicalContainerName(c)] = api.ImageSummary{
ID: id,
Repository: repository,
Tag: tag,
Platform: platforms.Platform{
Architecture: image.Architecture,
OS: image.Os,
OSVersion: image.OsVersion,
Variant: image.Variant,
},
Size: image.Size,
Created: created,
LastTagTime: image.Metadata.LastTagTime,
}
return nil
})
}
err = eg.Wait()
return summary, err
}
func (s *composeService) getImageSummaries(ctx context.Context, repoTags []string) (map[string]api.ImageSummary, error) {
summary := map[string]api.ImageSummary{}
l := sync.Mutex{}
eg, ctx := errgroup.WithContext(ctx)
for _, repoTag := range repoTags {
eg.Go(func() error {
inspect, err := s.apiClient().ImageInspect(ctx, repoTag)
if err != nil {
if errdefs.IsNotFound(err) {
return nil
}
return fmt.Errorf("unable to get image '%s': %w", repoTag, err)
}
tag := ""
repository := ""
ref, err := reference.ParseDockerRef(repoTag)
if err == nil {
// ParseDockerRef will reject a local image ID
repository = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
l.Lock()
summary[repoTag] = api.ImageSummary{
ID: inspect.ID,
Repository: repository,
Tag: tag,
Size: inspect.Size,
LastTagTime: inspect.Metadata.LastTagTime,
}
l.Unlock()
return nil
})
}
return summary, eg.Wait()
}
================================================
FILE: pkg/compose/images_test.go
================================================
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"net/netip"
"strings"
"testing"
"time"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v5/pkg/api"
)
func TestImages(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
args := projectFilter(strings.ToLower(testProject))
listOpts := client.ContainerListOptions{All: true, Filters: args}
api.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{APIVersion: "1.96"}, nil).AnyTimes()
timeStr1 := "2025-06-06T06:06:06.000000000Z"
created1, _ := time.Parse(time.RFC3339Nano, timeStr1)
timeStr2 := "2025-03-03T03:03:03.000000000Z"
created2, _ := time.Parse(time.RFC3339Nano, timeStr2)
image1 := imageInspect("image1", "foo:1", 12345, timeStr1)
image2 := imageInspect("image2", "bar:2", 67890, timeStr2)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(client.ImageInspectResult{InspectResponse: image1}, nil).MaxTimes(2)
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(client.ImageInspectResult{InspectResponse: image2}, nil)
c1 := containerDetail("service1", "123", container.StateRunning, "foo:1")
c2 := containerDetail("service1", "456", container.StateRunning, "bar:2")
c2.Ports = []container.PortSummary{{PublicPort: 80, PrivatePort: 90, IP: netip.MustParseAddr("127.0.0.1")}}
c3 := containerDetail("service2", "789", container.StateExited, "foo:1")
api.EXPECT().ContainerList(t.Context(), listOpts).Return(client.ContainerListResult{
Items: []container.Summary{c1, c2, c3},
}, nil)
images, err := tested.Images(t.Context(), strings.ToLower(testProject), compose.ImagesOptions{})
expected := map[string]compose.ImageSummary{
"123": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
Created: &created1,
},
"456": {
ID: "image2",
Repository: "bar",
Tag: "2",
Size: 67890,
Created: &created2,
},
"789": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
Created: &created1,
},
}
assert.NilError(t, err)
assert.DeepEqual(t, images, expected)
}
func imageInspect(id string, imageReference string, size int64, created string) image.InspectResponse {
return image.InspectResponse{
ID: id,
RepoTags: []string{
"someRepo:someTag",
imageReference,
},
Size: size,
Created: created,
}
}
func containerDetail(service string, id string, status container.ContainerState, imageName string) container.Summary {
return container.Summary{
ID: id,
Names: []string{"/" + id},
Image: imageName,
Labels: containerLabels(service, false),
State: status,
}
}
================================================
FILE: pkg/compose/kill.go
================================================
/*
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"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Kill(ctx context.Context, projectName string, options api.KillOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.kill(ctx, strings.ToLower(projectName), options)
}, "kill", s.events)
}
func (s *composeService) kill(ctx context.Context, projectName string, options api.KillOptions) error {
services := options.Services
var containers Containers
containers, err := s.getContainers(ctx, projectName, oneOffInclude, options.All, services...)
if err != nil {
return err
}
project := options.Project
if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil {
return err
}
}
if !options.RemoveOrphans {
containers = containers.filter(isService(project.ServiceNames()...))
}
if len(containers) == 0 {
return api.ErrNoResources
}
eg, ctx := errgroup.WithContext(ctx)
containers.forEach(func(ctr container.Summary) {
eg.Go(func() error {
eventName := getContainerProgressName(ctr)
s.events.On(killingEvent(eventName))
_, err := s.apiClient().ContainerKill(ctx, ctr.ID, client.ContainerKillOptions{
Signal: options.Signal,
})
if err != nil {
s.events.On(errorEvent(eventName, "Error while Killing"))
return err
}
s.events.On(killedEvent(eventName))
return nil
})
})
return eg.Wait()
}
================================================
FILE: pkg/compose/kill_test.go
================================================
/*
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"
"path/filepath"
"strings"
"testing"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v5/pkg/api"
)
const testProject = "testProject"
func TestKillAll(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
name := strings.ToLower(testProject)
api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{
Filters: projectFilter(name).Add("label", hasConfigHashLabel()),
}).Return(client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service1", "456", false),
testContainer("service2", "789", false),
},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{
Items: []network.Summary{{
Network: network.Network{ID: "abc123", Name: "testProject_default"},
}},
}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "123", client.ContainerKillOptions{}).Return(client.ContainerKillResult{}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "456", client.ContainerKillOptions{}).Return(client.ContainerKillResult{}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "789", client.ContainerKillOptions{}).Return(client.ContainerKillResult{}, nil)
err = tested.Kill(t.Context(), name, compose.KillOptions{})
assert.NilError(t, err)
}
func TestKillSignal(t *testing.T) {
const serviceName = "service1"
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
name := strings.ToLower(testProject)
listOptions := client.ContainerListOptions{
Filters: projectFilter(name).Add("label", serviceFilter(serviceName), hasConfigHashLabel()),
}
api.EXPECT().ContainerList(t.Context(), listOptions).Return(client.ContainerListResult{
Items: []container.Summary{testContainer(serviceName, "123", false)},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{
Items: []network.Summary{{
Network: network.Network{ID: "abc123", Name: "testProject_default"},
}},
}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "123", client.ContainerKillOptions{
Signal: "SIGTERM",
}).Return(client.ContainerKillResult{}, nil)
err = tested.Kill(t.Context(), name, compose.KillOptions{Services: []string{serviceName}, Signal: "SIGTERM"})
assert.NilError(t, err)
}
func testContainer(service string, id string, oneOff bool) container.Summary {
// canonical docker names in the API start with a leading slash, some
// parts of Compose code will attempt to strip this off, so make sure
// it's consistently present
name := "/" + strings.TrimPrefix(id, "/")
return container.Summary{
ID: id,
Names: []string{name},
Labels: containerLabels(service, oneOff),
State: container.StateExited,
}
}
func containerLabels(service string, oneOff bool) map[string]string {
workingdir := "/src/pkg/compose/testdata"
composefile := filepath.Join(workingdir, "compose.yaml")
labels := map[string]string{
compose.ServiceLabel: service,
compose.ConfigFilesLabel: composefile,
compose.WorkingDirLabel: workingdir,
compose.ProjectLabel: strings.ToLower(testProject),
}
if oneOff {
labels[compose.OneoffLabel] = "True"
}
return labels
}
func anyCancellableContext() gomock.Matcher {
//nolint:forbidigo // This creates a context type for gomock matching, not for actual test usage
ctxWithCancel, cancel := context.WithCancel(context.Background())
cancel()
return gomock.AssignableToTypeOf(ctxWithCancel)
}
func projectFilterListOpt(withOneOff bool) client.ContainerListOptions {
filter := projectFilter(strings.ToLower(testProject)).Add("label", hasConfigHashLabel())
if !withOneOff {
filter.Add("label", oneOffFilter(false))
}
return client.ContainerListOptions{
Filters: filter,
All: true,
}
}
================================================
FILE: pkg/compose/loader.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"os"
"strings"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/remote"
"github.com/docker/compose/v5/pkg/utils"
)
// LoadProject implements api.Compose.LoadProject
// It loads and validates a Compose project from configuration files.
func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
// Setup remote loaders (Git, OCI)
remoteLoaders := s.createRemoteLoaders(options)
projectOptions, err := s.buildProjectOptions(options, remoteLoaders)
if err != nil {
return nil, err
}
// Register all user-provided listeners (e.g., for metrics collection)
for _, listener := range options.LoadListeners {
if listener != nil {
projectOptions.WithListeners(listener)
}
}
if options.Compatibility || utils.StringToBool(projectOptions.Environment[api.ComposeCompatibility]) {
api.Separator = "_"
}
project, err := projectOptions.LoadProject(ctx)
if err != nil {
return nil, err
}
// Post-processing: service selection, environment resolution, etc.
project, err = s.postProcessProject(project, options)
if err != nil {
return nil, err
}
return project, nil
}
// createRemoteLoaders creates Git and OCI remote loaders if not in offline mode
func (s *composeService) createRemoteLoaders(options api.ProjectLoadOptions) []loader.ResourceLoader {
if options.Offline {
return nil
}
git := remote.NewGitRemoteLoader(s.dockerCli, options.Offline)
oci := remote.NewOCIRemoteLoader(s.dockerCli, options.Offline, options.OCI)
return []loader.ResourceLoader{git, oci}
}
// buildProjectOptions constructs compose-go ProjectOptions from API options
func (s *composeService) buildProjectOptions(options api.ProjectLoadOptions, remoteLoaders []loader.ResourceLoader) (*cli.ProjectOptions, error) {
opts := []cli.ProjectOptionsFn{
cli.WithWorkingDirectory(options.WorkingDir),
cli.WithOsEnv,
}
// Add PWD if not present
if _, present := os.LookupEnv("PWD"); !present {
if pwd, err := os.Getwd(); err == nil {
opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
}
}
// Add remote loaders
for _, r := range remoteLoaders {
opts = append(opts, cli.WithResourceLoader(r))
}
opts = append(opts,
// Load PWD/.env if present and no explicit --env-file has been set
cli.WithEnvFiles(options.EnvFiles...),
// read dot env file to populate project environment
cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
cli.WithDefaultConfigPath,
// .. and then, a project directory != PWD maybe has been set so let's load .env file
cli.WithEnvFiles(options.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
cli.WithDotEnv, //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
// eventually COMPOSE_PROFILES should have been set
cli.WithDefaultProfiles(options.Profiles...),
cli.WithName(options.ProjectName),
)
return cli.NewProjectOptions(options.ConfigPaths, append(options.ProjectOptionsFns, opts...)...)
}
// postProcessProject applies post-loading transformations to the project
func (s *composeService) postProcessProject(project *types.Project, options api.ProjectLoadOptions) (*types.Project, error) {
if project.Name == "" {
return nil, errors.New("project name can't be empty. Use ProjectName option to set a valid name")
}
project, err := project.WithServicesEnabled(options.Services...)
if err != nil {
return nil, err
}
// Add custom labels
for name, s := range project.Services {
s.CustomLabels = map[string]string{
api.ProjectLabel: project.Name,
api.ServiceLabel: name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: "False",
}
if len(options.EnvFiles) != 0 {
s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(options.EnvFiles, ",")
}
project.Services[name] = s
}
project, err = project.WithSelectedServices(options.Services)
if err != nil {
return nil, err
}
// Remove unnecessary resources if not All
if !options.All {
project = project.WithoutUnnecessaryResources()
}
return project, nil
}
================================================
FILE: pkg/compose/loader_test.go
================================================
/*
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 (
"os"
"path/filepath"
"testing"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v5/pkg/api"
)
func TestLoadProject_Basic(t *testing.T) {
// Create a temporary compose file
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
name: test-project
services:
web:
image: nginx:latest
ports:
- "8080:80"
db:
image: postgres:latest
environment:
POSTGRES_PASSWORD: secret
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Load the project
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
})
// Assertions
require.NoError(t, err)
assert.NotNil(t, project)
assert.Equal(t, "test-project", project.Name)
assert.Len(t, project.Services, 2)
assert.Contains(t, project.Services, "web")
assert.Contains(t, project.Services, "db")
// Check labels were applied
webService := project.Services["web"]
assert.Equal(t, "test-project", webService.CustomLabels[api.ProjectLabel])
assert.Equal(t, "web", webService.CustomLabels[api.ServiceLabel])
}
func TestLoadProject_WithEnvironmentResolution(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
app:
image: myapp:latest
environment:
- TEST_VAR=${TEST_VAR}
- LITERAL_VAR=literal_value
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
// Set environment variable
t.Setenv("TEST_VAR", "resolved_value")
service, err := NewComposeService(nil)
require.NoError(t, err)
// Test with environment resolution (default)
t.Run("WithResolution", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
})
require.NoError(t, err)
appService := project.Services["app"]
// Environment should be resolved
assert.NotNil(t, appService.Environment["TEST_VAR"])
assert.Equal(t, "resolved_value", *appService.Environment["TEST_VAR"])
assert.NotNil(t, appService.Environment["LITERAL_VAR"])
assert.Equal(t, "literal_value", *appService.Environment["LITERAL_VAR"])
})
// Test without environment resolution
t.Run("WithoutResolution", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
ProjectOptionsFns: []cli.ProjectOptionsFn{cli.WithoutEnvironmentResolution},
})
require.NoError(t, err)
appService := project.Services["app"]
// Environment should NOT be resolved, keeping raw values
// Note: This depends on compose-go behavior, which may still have some resolution
assert.NotNil(t, appService.Environment)
})
}
func TestLoadProject_ServiceSelection(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
web:
image: nginx:latest
db:
image: postgres:latest
cache:
image: redis:latest
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Load only specific services
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
Services: []string{"web", "db"},
})
require.NoError(t, err)
assert.Len(t, project.Services, 2)
assert.Contains(t, project.Services, "web")
assert.Contains(t, project.Services, "db")
assert.NotContains(t, project.Services, "cache")
}
func TestLoadProject_WithProfiles(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
web:
image: nginx:latest
debug:
image: busybox:latest
profiles: ["debug"]
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Without debug profile
t.Run("WithoutProfile", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
})
require.NoError(t, err)
assert.Len(t, project.Services, 1)
assert.Contains(t, project.Services, "web")
})
// With debug profile
t.Run("WithProfile", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
Profiles: []string{"debug"},
})
require.NoError(t, err)
assert.Len(t, project.Services, 2)
assert.Contains(t, project.Services, "web")
assert.Contains(t, project.Services, "debug")
})
}
func TestLoadProject_WithLoadListeners(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
web:
image: nginx:latest
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Track events received
var events []string
listener := func(event string, metadata map[string]any) {
events = append(events, event)
}
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
LoadListeners: []api.LoadListener{listener},
})
require.NoError(t, err)
assert.NotNil(t, project)
// Listeners should have been called (exact events depend on compose-go implementation)
// The slice itself is always initialized (non-nil), even if empty
_ = events // events may or may not have entries depending on compose-go behavior
}
func TestLoadProject_ProjectNameInference(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
web:
image: nginx:latest
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Without explicit project name
t.Run("InferredName", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
})
require.NoError(t, err)
// Project name should be inferred from directory
assert.NotEmpty(t, project.Name)
})
// With explicit project name
t.Run("ExplicitName", func(t *testing.T) {
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
ProjectName: "my-custom-project",
})
require.NoError(t, err)
assert.Equal(t, "my-custom-project", project.Name)
})
}
func TestLoadProject_Compatibility(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
services:
web:
image: nginx:latest
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// With compatibility mode
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
Compatibility: true,
})
require.NoError(t, err)
assert.NotNil(t, project)
// In compatibility mode, separator should be "_"
assert.Equal(t, "_", api.Separator)
// Reset separator
api.Separator = "-"
}
func TestLoadProject_InvalidComposeFile(t *testing.T) {
tmpDir := t.TempDir()
composeFile := filepath.Join(tmpDir, "compose.yaml")
composeContent := `
this is not valid yaml: [[[
`
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
require.NoError(t, err)
service, err := NewComposeService(nil)
require.NoError(t, err)
// Should return an error for invalid YAML
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{composeFile},
})
require.Error(t, err)
assert.Nil(t, project)
}
func TestLoadProject_MissingComposeFile(t *testing.T) {
service, err := NewComposeService(nil)
require.NoError(t, err)
// Should return an error for missing file
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{"/nonexistent/compose.yaml"},
})
require.Error(t, err)
assert.Nil(t, project)
}
================================================
FILE: pkg/compose/logs.go
================================================
/*
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"
"io"
"github.com/containerd/errdefs"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func (s *composeService) Logs(
ctx context.Context,
projectName string,
consumer api.LogConsumer,
options api.LogOptions,
) error {
var containers Containers
var err error
if options.Index > 0 {
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, options.Services[0], options.Index)
if err != nil {
return err
}
containers = append(containers, ctr)
} else {
containers, err = s.getContainers(ctx, projectName, oneOffExclude, true, options.Services...)
if err != nil {
return err
}
}
if options.Project != nil && len(options.Services) == 0 {
// we run with an explicit compose.yaml, so only consider services defined in this file
options.Services = options.Project.ServiceNames()
containers = containers.filter(isService(options.Services...))
}
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
err := s.logContainer(ctx, consumer, ctr, options)
if errdefs.IsNotImplemented(err) {
logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
return nil
}
return err
})
}
if options.Follow {
printer := newLogPrinter(consumer)
monitor := newMonitor(s.apiClient(), projectName)
if len(options.Services) > 0 {
monitor.withServices(options.Services)
} else if options.Project != nil {
monitor.withServices(options.Project.ServiceNames())
}
monitor.withListener(printer.HandleEvent)
monitor.withListener(func(event api.ContainerEvent) {
if event.Type == api.ContainerEventStarted {
eg.Go(func() error {
res, err := s.apiClient().ContainerInspect(ctx, event.ID, client.ContainerInspectOptions{})
if err != nil {
return err
}
err = s.doLogContainer(ctx, consumer, event.Source, res.Container, api.LogOptions{
Follow: options.Follow,
Since: res.Container.State.StartedAt,
Until: options.Until,
Tail: options.Tail,
Timestamps: options.Timestamps,
})
if errdefs.IsNotImplemented(err) {
// ignore
return nil
}
return err
})
}
})
eg.Go(func() error {
// pass ctx so monitor will immediately stop on SIGINT
return monitor.Start(ctx)
})
}
return eg.Wait()
}
func (s *composeService) logContainer(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error {
res, err := s.apiClient().ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err != nil {
return err
}
name := getContainerNameWithoutProject(c)
return s.doLogContainer(ctx, consumer, name, res.Container, options)
}
func (s *composeService) doLogContainer(ctx context.Context, consumer api.LogConsumer, name string, ctr container.InspectResponse, options api.LogOptions) error {
r, err := s.apiClient().ContainerLogs(ctx, ctr.ID, client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: options.Follow,
Since: options.Since,
Until: options.Until,
Tail: options.Tail,
Timestamps: options.Timestamps,
})
if err != nil {
return err
}
defer r.Close() //nolint:errcheck
w := utils.GetWriter(func(line string) {
consumer.Log(name, line)
})
if ctr.Config.Tty {
_, err = io.Copy(w, r)
} else {
_, err = stdcopy.StdCopy(w, w, r)
}
return err
}
================================================
FILE: pkg/compose/logs_test.go
================================================
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"io"
"strings"
"sync"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/pkg/stdcopy"
containerType "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
compose "github.com/docker/compose/v5/pkg/api"
)
func TestComposeService_Logs_Demux(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
require.NoError(t, err)
name := strings.ToLower(testProject)
api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{
All: true,
Filters: projectFilter(name).Add("label", oneOffFilter(false), hasConfigHashLabel()),
}).Return(
client.ContainerListResult{
Items: []containerType.Summary{
testContainer("service", "c", false),
},
},
nil,
)
api.EXPECT().
ContainerInspect(anyCancellableContext(), "c", gomock.Any()).
Return(client.ContainerInspectResult{
Container: containerType.InspectResponse{
ID: "c",
Config: &containerType.Config{Tty: false},
},
}, nil)
c1Reader, c1Writer := io.Pipe()
t.Cleanup(func() {
_ = c1Reader.Close()
_ = c1Writer.Close()
})
c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout)
c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr)
go func() {
_, err := c1Stdout.Write([]byte("hello stdout\n"))
assert.NoError(t, err, "Writing to fake stdout")
_, err = c1Stderr.Write([]byte("hello stderr\n"))
assert.NoError(t, err, "Writing to fake stderr")
_ = c1Writer.Close()
}()
api.EXPECT().ContainerLogs(anyCancellableContext(), "c", gomock.Any()).
Return(c1Reader, nil)
opts := compose.LogOptions{
Project: &types.Project{
Services: types.Services{
"service": {Name: "service"},
},
},
}
consumer := &testLogConsumer{}
err = tested.Logs(t.Context(), name, consumer, opts)
require.NoError(t, err)
require.Equal(
t,
[]string{"hello stdout", "hello stderr"},
consumer.LogsForContainer("c"),
)
}
// TestComposeService_Logs_ServiceFiltering ensures that we do not include
// logs from out-of-scope services based on the Compose file vs actual state.
//
// NOTE(milas): This test exists because each method is currently duplicating
// a lot of the project/service filtering logic. We should consider moving it
// to an earlier point in the loading process, at which point this test could
// safely be removed.
func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
require.NoError(t, err)
name := strings.ToLower(testProject)
api.EXPECT().ContainerList(t.Context(), client.ContainerListOptions{
All: true,
Filters: projectFilter(name).Add("label", oneOffFilter(false), hasConfigHashLabel()),
}).Return(
client.ContainerListResult{
Items: []containerType.Summary{
testContainer("serviceA", "c1", false),
testContainer("serviceA", "c2", false),
// serviceB will be filtered out by the project definition to
// ensure we ignore "orphan" containers
testContainer("serviceB", "c3", false),
testContainer("serviceC", "c4", false),
},
},
nil,
)
for _, id := range []string{"c1", "c2", "c4"} {
api.EXPECT().
ContainerInspect(anyCancellableContext(), id, gomock.Any()).
Return(
client.ContainerInspectResult{
Container: containerType.InspectResponse{
ID: id,
Config: &containerType.Config{Tty: true},
},
},
nil,
)
api.EXPECT().ContainerLogs(anyCancellableContext(), id, gomock.Any()).
Return(io.NopCloser(strings.NewReader("hello "+id+"\n")), nil).
Times(1)
}
// this simulates passing `--filename` with a Compose file that does NOT
// reference `serviceB` even though it has running services for this proj
proj := &types.Project{
Services: types.Services{
"serviceA": {Name: "serviceA"},
"serviceC": {Name: "serviceC"},
},
}
consumer := &testLogConsumer{}
opts := compose.LogOptions{
Project: proj,
}
err = tested.Logs(t.Context(), name, consumer, opts)
require.NoError(t, err)
require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("c1"))
require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("c2"))
require.Empty(t, consumer.LogsForContainer("c3"))
require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("c4"))
}
type testLogConsumer struct {
mu sync.Mutex
// logs is keyed by container ID; values are log lines
logs map[string][]string
}
func (l *testLogConsumer) Log(containerName, message string) {
l.mu.Lock()
defer l.mu.Unlock()
if l.logs == nil {
l.logs = make(map[string][]string)
}
l.logs[containerName] = append(l.logs[containerName], message)
}
func (l *testLogConsumer) Err(containerName, message string) {
l.Log(containerName, message)
}
func (l *testLogConsumer) Status(containerName, msg string) {}
func (l *testLogConsumer) LogsForContainer(containerName string) []string {
l.mu.Lock()
defer l.mu.Unlock()
return l.logs[containerName]
}
================================================
FILE: pkg/compose/ls.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) List(ctx context.Context, opts api.ListOptions) ([]api.Stack, error) {
list, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: make(client.Filters).Add("label", api.ProjectLabel).Add("label", api.ConfigHashLabel),
All: opts.All,
})
if err != nil {
return nil, err
}
return containersToStacks(list.Items)
}
func containersToStacks(containers []container.Summary) ([]api.Stack, error) {
containersByLabel, keys, err := groupContainerByLabel(containers, api.ProjectLabel)
if err != nil {
return nil, err
}
var projects []api.Stack
for _, project := range keys {
configFiles, err := combinedConfigFiles(containersByLabel[project])
if err != nil {
logrus.Warn(err.Error())
configFiles = "N/A"
}
projects = append(projects, api.Stack{
ID: project,
Name: project,
Status: combinedStatus(containerToState(containersByLabel[project])),
ConfigFiles: configFiles,
})
}
return projects, nil
}
func combinedConfigFiles(containers []container.Summary) (string, error) {
configFiles := []string{}
for _, c := range containers {
files, ok := c.Labels[api.ConfigFilesLabel]
if !ok {
return "", fmt.Errorf("no label %q set on container %q of compose project", api.ConfigFilesLabel, c.ID)
}
for f := range strings.SplitSeq(files, ",") {
if !slices.Contains(configFiles, f) {
configFiles = append(configFiles, f)
}
}
}
return strings.Join(configFiles, ","), nil
}
func containerToState(containers []container.Summary) []string {
statuses := []string{}
for _, c := range containers {
statuses = append(statuses, string(c.State))
}
return statuses
}
func combinedStatus(statuses []string) string {
nbByStatus := map[string]int{}
keys := []string{}
for _, status := range statuses {
nb, ok := nbByStatus[status]
if !ok {
nb = 0
keys = append(keys, status)
}
nbByStatus[status] = nb + 1
}
sort.Strings(keys)
result := ""
for _, status := range keys {
nb := nbByStatus[status]
if result != "" {
result += ", "
}
result += fmt.Sprintf("%s(%d)", status, nb)
}
return result
}
func groupContainerByLabel(containers []container.Summary, labelName string) (map[string][]container.Summary, []string, error) {
containersByLabel := map[string][]container.Summary{}
keys := []string{}
for _, c := range containers {
label, ok := c.Labels[labelName]
if !ok {
return nil, nil, fmt.Errorf("no label %q set on container %q of compose project", labelName, c.ID)
}
labelContainers, ok := containersByLabel[label]
if !ok {
labelContainers = []container.Summary{}
keys = append(keys, label)
}
labelContainers = append(labelContainers, c)
containersByLabel[label] = labelContainers
}
sort.Strings(keys)
return containersByLabel, keys, nil
}
================================================
FILE: pkg/compose/ls_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"testing"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestContainersToStacks(t *testing.T) {
containers := []container.Summary{
{
ID: "service1",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service2",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service3",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
},
}
stacks, err := containersToStacks(containers)
assert.NilError(t, err)
assert.DeepEqual(t, stacks, []api.Stack{
{
ID: "project1",
Name: "project1",
Status: "running(2)",
ConfigFiles: "/home/docker-compose.yaml",
},
{
ID: "project2",
Name: "project2",
Status: "running(1)",
ConfigFiles: "/home/project2-docker-compose.yaml",
},
})
}
func TestStacksMixedStatus(t *testing.T) {
assert.Equal(t, combinedStatus([]string{"running"}), "running(1)")
assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
}
func TestCombinedConfigFiles(t *testing.T) {
containersByLabel := map[string][]container.Summary{
"project1": {
{
ID: "service1",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service2",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
},
"project2": {
{
ID: "service3",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
},
},
"project3": {
{
ID: "service4",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project3"},
},
},
}
testData := map[string]struct {
ConfigFiles string
Error error
}{
"project1": {ConfigFiles: "/home/docker-compose.yaml", Error: nil},
"project2": {ConfigFiles: "/home/project2-docker-compose.yaml", Error: nil},
"project3": {ConfigFiles: "", Error: fmt.Errorf("no label %q set on container %q of compose project", api.ConfigFilesLabel, "service4")},
}
for project, containers := range containersByLabel {
configFiles, err := combinedConfigFiles(containers)
expected := testData[project]
if expected.Error != nil {
assert.Error(t, err, expected.Error.Error())
} else {
assert.Equal(t, err, expected.Error)
}
assert.Equal(t, configFiles, expected.ConfigFiles)
}
}
================================================
FILE: pkg/compose/model.go
================================================
/*
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 (
"bufio"
"context"
"encoding/json"
"fmt"
"os/exec"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/docker/api/types/versions"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) ensureModels(ctx context.Context, project *types.Project, quietPull bool) error {
if len(project.Models) == 0 {
return nil
}
mdlAPI, err := s.newModelAPI(project)
if err != nil {
return err
}
defer mdlAPI.Close()
availableModels, err := mdlAPI.ListModels(ctx)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
return mdlAPI.SetModelVariables(ctx, project)
})
for name, config := range project.Models {
if config.Name == "" {
config.Name = name
}
eg.Go(func() error {
if !slices.Contains(availableModels, config.Model) {
err = mdlAPI.PullModel(ctx, config, quietPull, s.events)
if err != nil {
return err
}
}
return mdlAPI.ConfigureModel(ctx, config, s.events)
})
}
return eg.Wait()
}
type modelAPI struct {
path string
env []string
prepare func(ctx context.Context, cmd *exec.Cmd) error
cleanup func()
version string
}
func (s *composeService) newModelAPI(project *types.Project) (*modelAPI, error) {
dockerModel, err := manager.GetPlugin("model", s.dockerCli, &cobra.Command{})
if err != nil {
if errdefs.IsNotFound(err) {
return nil, fmt.Errorf("'models' support requires Docker Model plugin")
}
return nil, err
}
if dockerModel.Err != nil {
return nil, fmt.Errorf("failed to load Docker Model plugin: %w", dockerModel.Err)
}
endpoint, cleanup, err := s.propagateDockerEndpoint()
if err != nil {
return nil, err
}
return &modelAPI{
path: dockerModel.Path,
version: dockerModel.Version,
prepare: func(ctx context.Context, cmd *exec.Cmd) error {
return s.prepareShellOut(ctx, project.Environment, cmd)
},
cleanup: cleanup,
env: append(project.Environment.Values(), endpoint...),
}, nil
}
func (m *modelAPI) Close() {
m.cleanup()
}
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events api.EventProcessor) error {
events.On(api.Resource{
ID: model.Name,
Status: api.Working,
Text: api.StatusPulling,
})
cmd := exec.CommandContext(ctx, m.path, "pull", model.Model)
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
stream, err := cmd.StdoutPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
msg := scanner.Text()
if msg == "" {
continue
}
if !quietPull {
events.On(api.Resource{
ID: model.Name,
Status: api.Working,
Text: api.StatusPulling,
})
}
}
err = cmd.Wait()
if err != nil {
events.On(errorEvent(model.Name, err.Error()))
}
events.On(api.Resource{
ID: model.Name,
Status: api.Working,
Text: api.StatusPulled,
})
return err
}
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events api.EventProcessor) error {
events.On(api.Resource{
ID: config.Name,
Status: api.Working,
Text: api.StatusConfiguring,
})
// configure [--context-size=] MODEL [-- ]
args := []string{"configure"}
if config.ContextSize > 0 {
args = append(args, "--context-size", strconv.Itoa(config.ContextSize))
}
args = append(args, config.Model)
// Only append RuntimeFlags if docker model CLI version is >= v1.0.6
if len(config.RuntimeFlags) != 0 && m.supportsRuntimeFlags() {
args = append(args, "--")
args = append(args, config.RuntimeFlags...)
}
cmd := exec.CommandContext(ctx, m.path, args...)
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
err = cmd.Run()
if err != nil {
events.On(errorEvent(config.Name, err.Error()))
return err
}
events.On(api.Resource{
ID: config.Name,
Status: api.Done,
Text: api.StatusConfigured,
})
return nil
}
func (m *modelAPI) SetModelVariables(ctx context.Context, project *types.Project) error {
cmd := exec.CommandContext(ctx, m.path, "status", "--json")
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
statusOut, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error checking docker-model status: %w", err)
}
type Status struct {
Endpoint string `json:"endpoint"`
}
var status Status
err = json.Unmarshal(statusOut, &status)
if err != nil {
return err
}
for _, service := range project.Services {
for ref, modelConfig := range service.Models {
model := project.Models[ref]
varPrefix := strings.ReplaceAll(strings.ToUpper(ref), "-", "_")
var variable string
if modelConfig != nil && modelConfig.ModelVariable != "" {
variable = modelConfig.ModelVariable
} else {
variable = varPrefix + "_MODEL"
}
service.Environment[variable] = &model.Model
if modelConfig != nil && modelConfig.EndpointVariable != "" {
variable = modelConfig.EndpointVariable
} else {
variable = varPrefix + "_URL"
}
service.Environment[variable] = &status.Endpoint
}
}
return nil
}
type Model struct {
Id string `json:"id"`
Tags []string `json:"tags"`
Created int `json:"created"`
Config struct {
Format string `json:"format"`
Quantization string `json:"quantization"`
Parameters string `json:"parameters"`
Architecture string `json:"architecture"`
Size string `json:"size"`
} `json:"config"`
}
func (m *modelAPI) ListModels(ctx context.Context) ([]string, error) {
cmd := exec.CommandContext(ctx, m.path, "ls", "--json")
err := m.prepare(ctx, cmd)
if err != nil {
return nil, err
}
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("error checking available models: %w", err)
}
type AvailableModel struct {
Id string `json:"id"`
Tags []string `json:"tags"`
Created int `json:"created"`
}
models := []AvailableModel{}
err = json.Unmarshal(output, &models)
if err != nil {
return nil, fmt.Errorf("error unmarshalling available models: %w", err)
}
var availableModels []string
for _, model := range models {
availableModels = append(availableModels, model.Tags...)
}
return availableModels, nil
}
// supportsRuntimeFlags checks if the docker model version supports runtime flags
// Runtime flags are supported in version >= v1.0.6
func (m *modelAPI) supportsRuntimeFlags() bool {
// If version is not cached, don't append runtime flags to be safe
if m.version == "" {
return false
}
// Strip 'v' prefix if present (e.g., "v1.0.6" -> "1.0.6")
versionStr := strings.TrimPrefix(m.version, "v")
return !versions.LessThan(versionStr, "1.0.6")
}
================================================
FILE: pkg/compose/monitor.go
================================================
/*
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"
"strconv"
"github.com/containerd/errdefs"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
type monitor struct {
apiClient client.APIClient
project string
// services tells us which service to consider and those we can ignore, maybe ran by a concurrent compose command
services map[string]bool
listeners []api.ContainerEventListener
}
func newMonitor(apiClient client.APIClient, project string) *monitor {
return &monitor{
apiClient: apiClient,
project: project,
services: map[string]bool{},
}
}
func (c *monitor) withServices(services []string) {
for _, name := range services {
c.services[name] = true
}
}
// Start runs monitor to detect application events and return after termination
//
//nolint:gocyclo
func (c *monitor) Start(ctx context.Context) error {
// collect initial application container
initialState, err := c.apiClient.ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: projectFilter(c.project).Add("label",
oneOffFilter(false),
hasConfigHashLabel(),
),
})
if err != nil {
return err
}
// containers is the set if container IDs the application is based on
containers := utils.Set[string]{}
for _, ctr := range initialState.Items {
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
}
restarting := utils.Set[string]{}
res := c.apiClient.Events(ctx, client.EventsListOptions{
Filters: projectFilter(c.project).Add("type", "container"),
})
for {
if len(containers) == 0 {
return nil
}
select {
case <-ctx.Done():
return nil
case err := <-res.Err:
return err
case event := <-res.Messages:
if len(c.services) > 0 && !c.services[event.Actor.Attributes[api.ServiceLabel]] {
continue
}
ctr, err := c.getContainerSummary(event)
if err != nil {
return err
}
switch event.Action {
case events.ActionCreate:
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
evtType := api.ContainerEventCreated
if _, ok := ctr.Labels[api.ContainerReplaceLabel]; ok {
evtType = api.ContainerEventRecreated
}
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, evtType))
}
logrus.Debugf("container %s created", ctr.Name)
case events.ActionStart:
restarted := restarting.Has(ctr.ID)
if restarted {
logrus.Debugf("container %s restarted", ctr.Name)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted, func(e *api.ContainerEvent) {
e.Restarting = restarted
}))
}
} else {
logrus.Debugf("container %s started", ctr.Name)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted))
}
}
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
case events.ActionRestart:
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventRestarted))
}
logrus.Debugf("container %s restarted", ctr.Name)
case events.ActionDie:
logrus.Debugf("container %s exited with code %d", ctr.Name, ctr.ExitCode)
inspect, err := c.apiClient.ContainerInspect(ctx, event.Actor.ID, client.ContainerInspectOptions{})
if errdefs.IsNotFound(err) {
// Source is already removed
} else if err != nil {
return err
}
if inspect.Container.State != nil && (inspect.Container.State.Restarting || inspect.Container.State.Running) {
// State.Restarting is set by engine when container is configured to restart on exit
// on ContainerRestart it doesn't (see https://github.com/moby/moby/issues/45538)
// container state still is reported as "running"
logrus.Debugf("container %s is restarting", ctr.Name)
restarting.Add(ctr.ID)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited, func(e *api.ContainerEvent) {
e.Restarting = true
}))
}
} else {
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited))
}
containers.Remove(ctr.ID)
}
}
}
}
}
func newContainerEvent(timeNano int64, ctr *api.ContainerSummary, eventType int, opts ...func(e *api.ContainerEvent)) api.ContainerEvent {
name := ctr.Name
defaultName := getDefaultContainerName(ctr.Project, ctr.Labels[api.ServiceLabel], ctr.Labels[api.ContainerNumberLabel])
if name == defaultName {
// remove project- prefix
name = name[len(ctr.Project)+1:]
}
event := api.ContainerEvent{
Type: eventType,
Container: ctr,
Time: timeNano,
Source: name,
ID: ctr.ID,
Service: ctr.Service,
ExitCode: ctr.ExitCode,
}
for _, opt := range opts {
opt(&event)
}
return event
}
func (c *monitor) getContainerSummary(event events.Message) (*api.ContainerSummary, error) {
ctr := &api.ContainerSummary{
ID: event.Actor.ID,
Name: event.Actor.Attributes["name"],
Project: c.project,
Service: event.Actor.Attributes[api.ServiceLabel],
Labels: event.Actor.Attributes, // More than just labels, but that'c the closest the API gives us
}
if ec, ok := event.Actor.Attributes["exitCode"]; ok {
exitCode, err := strconv.Atoi(ec)
if err != nil {
return nil, err
}
ctr.ExitCode = exitCode
}
return ctr, nil
}
func (c *monitor) withListener(listener api.ContainerEventListener) {
c.listeners = append(c.listeners, listener)
}
================================================
FILE: pkg/compose/pause.go
================================================
/*
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"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Pause(ctx context.Context, projectName string, options api.PauseOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.pause(ctx, strings.ToLower(projectName), options)
}, "pause", s.events)
}
func (s *composeService) pause(ctx context.Context, projectName string, options api.PauseOptions) error {
containers, err := s.getContainers(ctx, projectName, oneOffExclude, false, options.Services...)
if err != nil {
return err
}
if options.Project != nil {
containers = containers.filter(isService(options.Project.ServiceNames()...))
}
eg, ctx := errgroup.WithContext(ctx)
containers.forEach(func(container container.Summary) {
eg.Go(func() error {
_, err := s.apiClient().ContainerPause(ctx, container.ID, client.ContainerPauseOptions{})
if err == nil {
eventName := getContainerProgressName(container)
s.events.On(newEvent(eventName, api.Done, "Paused"))
}
return err
})
})
return eg.Wait()
}
func (s *composeService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.unPause(ctx, strings.ToLower(projectName), options)
}, "unpause", s.events)
}
func (s *composeService) unPause(ctx context.Context, projectName string, options api.PauseOptions) error {
containers, err := s.getContainers(ctx, projectName, oneOffExclude, false, options.Services...)
if err != nil {
return err
}
if options.Project != nil {
containers = containers.filter(isService(options.Project.ServiceNames()...))
}
eg, ctx := errgroup.WithContext(ctx)
containers.forEach(func(ctr container.Summary) {
eg.Go(func() error {
_, err = s.apiClient().ContainerUnpause(ctx, ctr.ID, client.ContainerUnpauseOptions{})
if err == nil {
eventName := getContainerProgressName(ctr)
s.events.On(newEvent(eventName, api.Done, "Unpaused"))
}
return err
})
})
return eg.Wait()
}
================================================
FILE: pkg/compose/plugins.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
)
type JsonMessage struct {
Type string `json:"type"`
Message string `json:"message"`
}
const (
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
DebugType = "debug"
providerMetadataDirectory = "compose/providers"
)
var mux sync.Mutex
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
provider := *service.Provider
plugin, err := s.getPluginBinaryPath(provider.Type)
if err != nil {
return err
}
cmd, err := s.setupPluginCommand(ctx, project, service, plugin, command)
if err != nil {
return err
}
variables, err := s.executePlugin(cmd, command, service)
if err != nil {
return err
}
mux.Lock()
defer mux.Unlock()
for name, s := range project.Services {
if _, ok := s.DependsOn[service.Name]; ok {
prefix := strings.ToUpper(service.Name) + "_"
for key, val := range variables {
s.Environment[prefix+key] = &val
}
project.Services[name] = s
}
}
return nil
}
func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service types.ServiceConfig) (types.Mapping, error) {
var action string
switch command {
case "up":
s.events.On(creatingEvent(service.Name))
action = "create"
case "down":
s.events.On(removingEvent(service.Name))
action = "remove"
default:
return nil, fmt.Errorf("unsupported plugin command: %s", command)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
decoder := json.NewDecoder(stdout)
defer func() { _ = stdout.Close() }()
variables := types.Mapping{}
for {
var msg JsonMessage
err = decoder.Decode(&msg)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
switch msg.Type {
case ErrorType:
s.events.On(newEvent(service.Name, api.Error, msg.Message))
return nil, errors.New(msg.Message)
case InfoType:
s.events.On(newEvent(service.Name, api.Working, msg.Message))
case SetEnvType:
key, val, found := strings.Cut(msg.Message, "=")
if !found {
return nil, fmt.Errorf("invalid response from plugin: %s", msg.Message)
}
variables[key] = val
case DebugType:
logrus.Debugf("%s: %s", service.Name, msg.Message)
default:
return nil, fmt.Errorf("invalid response from plugin: %s", msg.Type)
}
}
err = cmd.Wait()
if err != nil {
s.events.On(errorEvent(service.Name, err.Error()))
return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error())
}
switch command {
case "up":
s.events.On(createdEvent(service.Name))
case "down":
s.events.On(removedEvent(service.Name))
}
return variables, nil
}
func (s *composeService) getPluginBinaryPath(provider string) (path string, err error) {
if provider == "compose" {
return "", errors.New("'compose' is not a valid provider type")
}
plugin, err := manager.GetPlugin(provider, s.dockerCli, &cobra.Command{})
if err == nil {
path = plugin.Path
}
if errdefs.IsNotFound(err) {
path, err = exec.LookPath(executable(provider))
}
return path, err
}
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) (*exec.Cmd, error) {
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type, project)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
currentCommandMetadata = cmdOptionsMetadata.Up
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
provider := *service.Provider
commandMetadataIsEmpty := cmdOptionsMetadata.IsEmpty()
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
return nil, err
}
args := []string{"compose", fmt.Sprintf("--project-name=%s", project.Name), command}
for k, v := range provider.Options {
for _, value := range v {
if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok {
args = append(args, fmt.Sprintf("--%s=%s", k, value))
}
}
}
args = append(args, service.Name)
cmd := exec.CommandContext(ctx, path, args...)
err := s.prepareShellOut(ctx, project.Environment, cmd)
if err != nil {
return nil, err
}
return cmd, nil
}
func (s *composeService) getPluginMetadata(path, command string, project *types.Project) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
err := s.prepareShellOut(context.Background(), project.Environment, cmd)
if err != nil {
logrus.Debugf("failed to prepare plugin metadata command: %v", err)
return ProviderMetadata{}
}
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
logrus.Debugf("failed to start plugin metadata command: %v", err)
return ProviderMetadata{}
}
var metadata ProviderMetadata
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
output, _ := io.ReadAll(stdout)
logrus.Debugf("failed to decode plugin metadata: %v - %s", err, output)
return ProviderMetadata{}
}
// Save metadata into docker home directory to be used by Docker LSP tool
// Just log the error as it's not a critical error for the main flow
metadataDir := filepath.Join(config.Dir(), providerMetadataDirectory)
if err := os.MkdirAll(metadataDir, 0o700); err == nil {
metadataFilePath := filepath.Join(metadataDir, command+".json")
if err := os.WriteFile(metadataFilePath, stdout.Bytes(), 0o600); err != nil {
logrus.Debugf("failed to save plugin metadata: %v", err)
}
} else {
logrus.Debugf("failed to create plugin metadata directory: %v", err)
}
return metadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
func (p ProviderMetadata) IsEmpty() bool {
return p.Description == "" && p.Up.Parameters == nil && p.Down.Parameters == nil
}
type CommandMetadata struct {
Parameters []ParameterMetadata `json:"parameters"`
}
type ParameterMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
func (c CommandMetadata) GetParameter(paramName string) (ParameterMetadata, bool) {
for _, p := range c.Parameters {
if p.Name == paramName {
return p, true
}
}
return ParameterMetadata{}, false
}
func (c CommandMetadata) CheckRequiredParameters(provider types.ServiceProviderConfig) error {
for _, p := range c.Parameters {
if p.Required {
if _, ok := provider.Options[p.Name]; !ok {
return fmt.Errorf("required parameter %q is missing from provider %q definition", p.Name, provider.Type)
}
}
}
return nil
}
================================================
FILE: pkg/compose/plugins_windows.go
================================================
//go:build windows
/*
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
func executable(s string) string {
return s + ".exe"
}
================================================
FILE: pkg/compose/port.go
================================================
/*
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"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Port(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (string, int, error) {
projectName = strings.ToLower(projectName)
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, service, options.Index)
if err != nil {
return "", 0, err
}
for _, p := range ctr.Ports {
if p.PrivatePort == port && p.Type == options.Protocol {
return p.IP.String(), int(p.PublicPort), nil
}
}
return "", 0, portNotFoundError(options.Protocol, port, ctr)
}
func portNotFoundError(protocol string, port uint16, ctr container.Summary) error {
formatPort := func(protocol string, port uint16) string {
return fmt.Sprintf("%d/%s", port, protocol)
}
var containerPorts []string
for _, p := range ctr.Ports {
containerPorts = append(containerPorts, formatPort(p.Type, p.PublicPort))
}
name := strings.TrimPrefix(ctr.Names[0], "/")
return fmt.Errorf("no port %s for container %s: %s", formatPort(protocol, port), name, strings.Join(containerPorts, ", "))
}
================================================
FILE: pkg/compose/printer.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"github.com/docker/compose/v5/pkg/api"
)
// logPrinter watch application containers and collect their logs
type logPrinter interface {
HandleEvent(event api.ContainerEvent)
}
type printer struct {
consumer api.LogConsumer
}
// newLogPrinter builds a LogPrinter passing containers logs to LogConsumer
func newLogPrinter(consumer api.LogConsumer) logPrinter {
printer := printer{
consumer: consumer,
}
return &printer
}
func (p *printer) HandleEvent(event api.ContainerEvent) {
switch event.Type {
case api.ContainerEventExited:
if event.Restarting {
p.consumer.Status(event.Source, fmt.Sprintf("exited with code %d (restarting)", event.ExitCode))
} else {
p.consumer.Status(event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
}
case api.ContainerEventRecreated:
p.consumer.Status(event.Container.Labels[api.ContainerReplaceLabel], "has been recreated")
case api.ContainerEventLog, api.HookEventLog:
p.consumer.Log(event.Source, event.Line)
case api.ContainerEventErr:
p.consumer.Err(event.Source, event.Line)
}
}
================================================
FILE: pkg/compose/progress.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/docker/compose/v5/pkg/api"
)
type progressFunc func(context.Context) error
func Run(ctx context.Context, pf progressFunc, operation string, bus api.EventProcessor) error {
bus.Start(ctx, operation)
err := pf(ctx)
bus.Done(operation, err != nil)
return err
}
// errorEvent creates a new Error Resource with message
func errorEvent(id string, msg string) api.Resource {
return api.Resource{
ID: id,
Status: api.Error,
Text: api.StatusError,
Details: msg,
}
}
// errorEventf creates a new Error Resource with format message
func errorEventf(id string, msg string, args ...any) api.Resource {
return errorEvent(id, fmt.Sprintf(msg, args...))
}
// creatingEvent creates a new Create in progress Resource
func creatingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusCreating)
}
// startingEvent creates a new Starting in progress Resource
func startingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusStarting)
}
// startedEvent creates a new Started in progress Resource
func startedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusStarted)
}
// waiting creates a new waiting event
func waiting(id string) api.Resource {
return newEvent(id, api.Working, api.StatusWaiting)
}
// healthy creates a new healthy event
func healthy(id string) api.Resource {
return newEvent(id, api.Done, api.StatusHealthy)
}
// exited creates a new exited event
func exited(id string) api.Resource {
return newEvent(id, api.Done, api.StatusExited)
}
// restartingEvent creates a new Restarting in progress Resource
func restartingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusRestarting)
}
// runningEvent creates a new Running in progress Resource
func runningEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusRunning)
}
// createdEvent creates a new Created (done) Resource
func createdEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusCreated)
}
// stoppingEvent creates a new Stopping in progress Resource
func stoppingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusStopping)
}
// stoppedEvent creates a new Stopping in progress Resource
func stoppedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusStopped)
}
// killingEvent creates a new Killing in progress Resource
func killingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusKilling)
}
// killedEvent creates a new Killed in progress Resource
func killedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusKilled)
}
// removingEvent creates a new Removing in progress Resource
func removingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusRemoving)
}
// removedEvent creates a new removed (done) Resource
func removedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusRemoved)
}
// buildingEvent creates a new Building in progress Resource
func buildingEvent(id string) api.Resource {
return newEvent("Image "+id, api.Working, api.StatusBuilding)
}
// builtEvent creates a new built (done) Resource
func builtEvent(id string) api.Resource {
return newEvent("Image "+id, api.Done, api.StatusBuilt)
}
// pullingEvent creates a new pulling (in progress) Resource
func pullingEvent(id string) api.Resource {
return newEvent("Image "+id, api.Working, api.StatusPulling)
}
// pulledEvent creates a new pulled (done) Resource
func pulledEvent(id string) api.Resource {
return newEvent("Image "+id, api.Done, api.StatusPulled)
}
// skippedEvent creates a new Skipped Resource
func skippedEvent(id string, reason string) api.Resource {
return api.Resource{
ID: id,
Status: api.Warning,
Text: "Skipped: " + reason,
}
}
// newEvent new event
func newEvent(id string, status api.EventStatus, text string, reason ...string) api.Resource {
r := api.Resource{
ID: id,
Status: status,
Text: text,
}
if len(reason) > 0 {
r.Details = reason[0]
}
return r
}
type ignore struct{}
func (q *ignore) Start(_ context.Context, _ string) {
}
func (q *ignore) Done(_ string, _ bool) {
}
func (q *ignore) On(_ ...api.Resource) {
}
================================================
FILE: pkg/compose/ps.go
================================================
/*
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"
"sort"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
//nolint:gocyclo
func (s *composeService) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
projectName = strings.ToLower(projectName)
oneOff := oneOffExclude
if options.All {
oneOff = oneOffInclude
}
containers, err := s.getContainers(ctx, projectName, oneOff, options.All, options.Services...)
if err != nil {
return nil, err
}
if len(options.Services) != 0 {
containers = containers.filter(isService(options.Services...))
}
summary := make([]api.ContainerSummary, len(containers))
eg, ctx := errgroup.WithContext(ctx)
for i, ctr := range containers {
eg.Go(func() error {
publishers := make([]api.PortPublisher, len(ctr.Ports))
sort.Slice(ctr.Ports, func(i, j int) bool {
return ctr.Ports[i].PrivatePort < ctr.Ports[j].PrivatePort
})
for i, p := range ctr.Ports {
var url string
if p.IP.IsValid() {
url = p.IP.String()
}
publishers[i] = api.PortPublisher{
URL: url, // TODO(thaJeztah); change this to a netip.Addr ??
TargetPort: int(p.PrivatePort),
PublishedPort: int(p.PublicPort),
Protocol: p.Type,
}
}
inspect, err := s.apiClient().ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
if err != nil {
return err
}
var (
health container.HealthStatus
exitCode int
)
if inspect.Container.State != nil {
switch inspect.Container.State.Status {
case container.StateRunning:
if inspect.Container.State.Health != nil {
health = inspect.Container.State.Health.Status
}
case container.StateExited, container.StateDead:
exitCode = inspect.Container.State.ExitCode
}
}
var (
local int
mounts []string
)
for _, m := range ctr.Mounts {
name := m.Name
if name == "" {
name = m.Source
}
if m.Driver == "local" {
local++
}
mounts = append(mounts, name)
}
var networks []string
if ctr.NetworkSettings != nil {
for k := range ctr.NetworkSettings.Networks {
networks = append(networks, k)
}
}
summary[i] = api.ContainerSummary{
ID: ctr.ID,
Name: getCanonicalContainerName(ctr),
Names: ctr.Names,
Image: ctr.Image,
Project: ctr.Labels[api.ProjectLabel],
Service: ctr.Labels[api.ServiceLabel],
Command: ctr.Command,
State: ctr.State,
Status: ctr.Status,
Created: ctr.Created,
Labels: ctr.Labels,
SizeRw: ctr.SizeRw,
SizeRootFs: ctr.SizeRootFs,
Mounts: mounts,
LocalVolumes: local,
Networks: networks,
Health: health,
ExitCode: exitCode,
Publishers: publishers,
}
return nil
})
}
return summary, eg.Wait()
}
================================================
FILE: pkg/compose/ps_test.go
================================================
/*
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 (
"net/netip"
"strings"
"testing"
containerType "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v5/pkg/api"
)
func TestPs(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
listOpts := client.ContainerListOptions{
Filters: projectFilter(strings.ToLower(testProject)).Add("label", hasConfigHashLabel(), oneOffFilter(false)),
All: false,
}
c1, inspect1 := containerDetails("service1", "123", containerType.StateRunning, containerType.Healthy, 0)
c2, inspect2 := containerDetails("service1", "456", containerType.StateRunning, "", 0)
c2.Ports = []containerType.PortSummary{{PublicPort: 80, PrivatePort: 90, IP: netip.MustParseAddr("127.0.0.1")}}
c3, inspect3 := containerDetails("service2", "789", containerType.StateExited, "", 130)
api.EXPECT().ContainerList(t.Context(), listOpts).Return(client.ContainerListResult{
Items: []containerType.Summary{c1, c2, c3},
}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "123", gomock.Any()).Return(client.ContainerInspectResult{Container: inspect1}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "456", gomock.Any()).Return(client.ContainerInspectResult{Container: inspect2}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "789", gomock.Any()).Return(client.ContainerInspectResult{Container: inspect3}, nil)
containers, err := tested.Ps(t.Context(), strings.ToLower(testProject), compose.PsOptions{})
expected := []compose.ContainerSummary{
{
ID: "123", Name: "123", Names: []string{"/123"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: containerType.StateRunning,
Health: containerType.Healthy,
Publishers: []compose.PortPublisher{},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service1",
},
},
{
ID: "456", Name: "456", Names: []string{"/456"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: containerType.StateRunning,
Publishers: []compose.PortPublisher{{URL: "127.0.0.1", TargetPort: 90, PublishedPort: 80}},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service1",
},
},
{
ID: "789", Name: "789", Names: []string{"/789"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
State: containerType.StateExited,
ExitCode: 130,
Publishers: []compose.PortPublisher{},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service2",
},
},
}
assert.NilError(t, err)
assert.DeepEqual(t, containers, expected)
}
func containerDetails(service string, id string, status containerType.ContainerState, health containerType.HealthStatus, exitCode int) (containerType.Summary, containerType.InspectResponse) {
ctr := containerType.Summary{
ID: id,
Names: []string{"/" + id},
Image: "foo",
Labels: containerLabels(service, false),
State: status,
}
inspect := containerType.InspectResponse{
State: &containerType.State{
Status: status,
Health: &containerType.Health{Status: health},
ExitCode: exitCode,
},
}
return ctr, inspect
}
================================================
FILE: pkg/compose/publish.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/DefangLabs/secret-detector/pkg/scanner"
"github.com/DefangLabs/secret-detector/pkg/secrets"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/internal/oci"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose/transform"
)
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.publish(ctx, project, repository, options)
}, "publish", s.events)
}
//nolint:gocyclo
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
project, err := project.WithProfiles([]string{"*"})
if err != nil {
return err
}
accept, err := s.preChecks(project, options)
if err != nil {
return err
}
if !accept {
return nil
}
err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
if err != nil {
return err
}
layers, err := s.createLayers(ctx, project, options)
if err != nil {
return err
}
s.events.On(api.Resource{
ID: repository,
Text: "publishing",
Status: api.Working,
})
if logrus.IsLevelEnabled(logrus.DebugLevel) {
logrus.Debug("publishing layers")
for _, layer := range layers {
indent, _ := json.MarshalIndent(layer, "", " ")
fmt.Println(string(indent))
}
}
if !s.dryRun {
named, err := reference.ParseDockerRef(repository)
if err != nil {
return err
}
var insecureRegistries []string
if options.InsecureRegistry {
insecureRegistries = append(insecureRegistries, reference.Domain(named))
}
resolver := oci.NewResolver(s.configFile(), insecureRegistries...)
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
if err != nil {
s.events.On(api.Resource{
ID: repository,
Text: "publishing",
Status: api.Error,
})
return err
}
if options.Application {
manifests := []v1.Descriptor{}
for _, service := range project.Services {
ref, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}
manifest, err := oci.Copy(ctx, resolver, ref, named)
if err != nil {
return err
}
manifests = append(manifests, manifest)
}
descriptor.Data = nil
index, err := json.Marshal(v1.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Subject: &descriptor,
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
})
if err != nil {
return err
}
imagesDescriptor := v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
ArtifactType: oci.ComposeProjectArtifactType,
Digest: digest.FromString(string(index)),
Size: int64(len(index)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
Data: index,
}
err = oci.Push(ctx, resolver, reference.TrimNamed(named), imagesDescriptor)
if err != nil {
return err
}
}
}
s.events.On(api.Resource{
ID: repository,
Text: "published",
Status: api.Done,
})
return nil
}
func (s *composeService) createLayers(ctx context.Context, project *types.Project, options api.PublishOptions) ([]v1.Descriptor, error) {
var layers []v1.Descriptor
extFiles := map[string]string{}
envFiles := map[string]string{}
for _, file := range project.ComposeFiles {
data, err := processFile(ctx, file, project, extFiles, envFiles)
if err != nil {
return nil, err
}
layerDescriptor := oci.DescriptorForComposeFile(file, data)
layers = append(layers, layerDescriptor)
}
extLayers, err := processExtends(ctx, project, extFiles)
if err != nil {
return nil, err
}
layers = append(layers, extLayers...)
if options.WithEnvironment {
layers = append(layers, envFileLayers(envFiles)...)
}
if options.ResolveImageDigests {
yaml, err := s.generateImageDigestsOverride(ctx, project)
if err != nil {
return nil, err
}
layerDescriptor := oci.DescriptorForComposeFile("image-digests.yaml", yaml)
layers = append(layers, layerDescriptor)
}
return layers, nil
}
func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]v1.Descriptor, error) {
var layers []v1.Descriptor
moreExtFiles := map[string]string{}
for xf, hash := range extFiles {
data, err := processFile(ctx, xf, project, moreExtFiles, nil)
if err != nil {
return nil, err
}
layerDescriptor := oci.DescriptorForComposeFile(hash, data)
layerDescriptor.Annotations["com.docker.compose.extends"] = "true"
layers = append(layers, layerDescriptor)
}
for f, hash := range moreExtFiles {
if _, ok := extFiles[f]; ok {
delete(moreExtFiles, f)
}
extFiles[f] = hash
}
if len(moreExtFiles) > 0 {
extLayers, err := processExtends(ctx, project, moreExtFiles)
if err != nil {
return nil, err
}
layers = append(layers, extLayers...)
}
return layers, nil
}
func processFile(ctx context.Context, file string, project *types.Project, extFiles map[string]string, envFiles map[string]string) ([]byte, error) {
f, err := os.ReadFile(file)
if err != nil {
return nil, err
}
base, err := loader.LoadWithContext(ctx, types.ConfigDetails{
WorkingDir: project.WorkingDir,
Environment: project.Environment,
ConfigFiles: []types.ConfigFile{
{
Filename: file,
Content: f,
},
},
}, func(options *loader.Options) {
options.SkipValidation = true
options.SkipExtends = true
options.SkipConsistencyCheck = true
options.ResolvePaths = true
options.SkipInclude = true
options.Profiles = project.Profiles
})
if err != nil {
return nil, err
}
for name, service := range base.Services {
for i, envFile := range service.EnvFiles {
hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envFile.Path)))
envFiles[envFile.Path] = hash
f, err = transform.ReplaceEnvFile(f, name, i, hash)
if err != nil {
return nil, err
}
}
if service.Extends == nil {
continue
}
xf := service.Extends.File
if xf == "" {
continue
}
if _, err = os.Stat(service.Extends.File); os.IsNotExist(err) {
// No local file, while we loaded the project successfully: This is actually a remote resource
continue
}
hash := fmt.Sprintf("%x.yaml", sha256.Sum256([]byte(xf)))
extFiles[xf] = hash
f, err = transform.ReplaceExtendsFile(f, name, hash)
if err != nil {
return nil, err
}
}
return f, nil
}
func (s *composeService) generateImageDigestsOverride(ctx context.Context, project *types.Project) ([]byte, error) {
project, err := project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient()))
if err != nil {
return nil, err
}
override := types.Project{
Services: types.Services{},
}
for name, service := range project.Services {
override.Services[name] = types.ServiceConfig{
Image: service.Image,
}
}
return override.MarshalYAML()
}
func (s *composeService) preChecks(project *types.Project, options api.PublishOptions) (bool, error) {
if ok, err := s.checkOnlyBuildSection(project); !ok || err != nil {
return false, err
}
bindMounts := s.checkForBindMount(project)
if len(bindMounts) > 0 {
b := strings.Builder{}
b.WriteString("you are about to publish bind mounts declaration within your OCI artifact.\n" +
"only the bind mount declarations will be added to the OCI artifact (not content)\n" +
"please double check that you are not mounting potential user's sensitive directories or data\n")
for key, val := range bindMounts {
b.WriteString(key)
for _, v := range val {
b.WriteString(v.String())
b.WriteRune('\n')
}
}
b.WriteString("Are you ok to publish these bind mount declarations?")
confirm, err := s.prompt(b.String(), false)
if err != nil || !confirm {
return false, err
}
}
detectedSecrets, err := s.checkForSensitiveData(project)
if err != nil {
return false, err
}
if len(detectedSecrets) > 0 {
b := strings.Builder{}
b.WriteString("you are about to publish sensitive data within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data\n")
for _, val := range detectedSecrets {
b.WriteString(val.Type)
b.WriteRune('\n')
b.WriteString(fmt.Sprintf("%q: %s\n", val.Key, val.Value))
}
b.WriteString("Are you ok to publish these sensitive data?")
confirm, err := s.prompt(b.String(), false)
if err != nil || !confirm {
return false, err
}
}
err = s.checkEnvironmentVariables(project, options)
if err != nil {
return false, err
}
return true, nil
}
func (s *composeService) checkEnvironmentVariables(project *types.Project, options api.PublishOptions) error {
errorList := map[string][]string{}
for _, service := range project.Services {
if len(service.EnvFiles) > 0 {
errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has env_file declared.", service.Name))
}
}
if !options.WithEnvironment && len(errorList) > 0 {
errorMsgSuffix := "To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,\n" +
"or remove sensitive data from your Compose configuration"
var errorMsg strings.Builder
for _, errors := range errorList {
for _, err := range errors {
errorMsg.WriteString(fmt.Sprintf("%s\n", err))
}
}
return fmt.Errorf("%s%s", errorMsg.String(), errorMsgSuffix)
}
return nil
}
func envFileLayers(files map[string]string) []v1.Descriptor {
var layers []v1.Descriptor
for file, hash := range files {
f, err := os.ReadFile(file)
if err != nil {
// if we can't read the file, skip to the next one
continue
}
layerDescriptor := oci.DescriptorForEnvFile(hash, f)
layers = append(layers, layerDescriptor)
}
return layers
}
func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, error) {
errorList := []string{}
for _, service := range project.Services {
if service.Image == "" && service.Build != nil {
errorList = append(errorList, service.Name)
}
}
if len(errorList) > 0 {
var errMsg strings.Builder
errMsg.WriteString("your Compose stack cannot be published as it only contains a build section for service(s):\n")
for _, serviceInError := range errorList {
errMsg.WriteString(fmt.Sprintf("- %q\n", serviceInError))
}
return false, errors.New(errMsg.String())
}
return true, nil
}
func (s *composeService) checkForBindMount(project *types.Project) map[string][]types.ServiceVolumeConfig {
allFindings := map[string][]types.ServiceVolumeConfig{}
for serviceName, config := range project.Services {
bindMounts := []types.ServiceVolumeConfig{}
for _, volume := range config.Volumes {
if volume.Type == types.VolumeTypeBind {
bindMounts = append(bindMounts, volume)
}
}
if len(bindMounts) > 0 {
allFindings[serviceName] = bindMounts
}
}
return allFindings
}
func (s *composeService) checkForSensitiveData(project *types.Project) ([]secrets.DetectedSecret, error) {
var allFindings []secrets.DetectedSecret
scan := scanner.NewDefaultScanner()
// Check all compose files
for _, file := range project.ComposeFiles {
in, err := composeFileAsByteReader(file, project)
if err != nil {
return nil, err
}
findings, err := scan.ScanReader(in)
if err != nil {
return nil, fmt.Errorf("failed to scan compose file %s: %w", file, err)
}
allFindings = append(allFindings, findings...)
}
for _, service := range project.Services {
// Check env files
for _, envFile := range service.EnvFiles {
findings, err := scan.ScanFile(envFile.Path)
if err != nil {
return nil, fmt.Errorf("failed to scan env file %s: %w", envFile.Path, err)
}
allFindings = append(allFindings, findings...)
}
}
// Check configs defined by files
for _, config := range project.Configs {
if config.File != "" {
findings, err := scan.ScanFile(config.File)
if err != nil {
return nil, fmt.Errorf("failed to scan config file %s: %w", config.File, err)
}
allFindings = append(allFindings, findings...)
}
}
// Check secrets defined by files
for _, secret := range project.Secrets {
if secret.File != "" {
findings, err := scan.ScanFile(secret.File)
if err != nil {
return nil, fmt.Errorf("failed to scan secret file %s: %w", secret.File, err)
}
allFindings = append(allFindings, findings...)
}
}
return allFindings, nil
}
func composeFileAsByteReader(filePath string, project *types.Project) (io.Reader, error) {
composeFile, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open compose file %s: %w", filePath, err)
}
base, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{
WorkingDir: project.WorkingDir,
Environment: project.Environment,
ConfigFiles: []types.ConfigFile{
{
Filename: filePath,
Content: composeFile,
},
},
}, func(options *loader.Options) {
options.SkipValidation = true
options.SkipExtends = true
options.SkipConsistencyCheck = true
options.ResolvePaths = true
options.SkipInterpolation = true
options.SkipResolveEnvironment = true
})
if err != nil {
return nil, err
}
in, err := base.MarshalYAML()
if err != nil {
return nil, err
}
return bytes.NewBuffer(in), nil
}
================================================
FILE: pkg/compose/publish_test.go
================================================
/*
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 (
"slices"
"testing"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/google/go-cmp/cmp"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/pkg/api"
)
func Test_createLayers(t *testing.T) {
project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{
WorkingDir: "testdata/publish/",
Environment: types.Mapping{},
ConfigFiles: []types.ConfigFile{
{
Filename: "testdata/publish/compose.yaml",
},
},
})
assert.NilError(t, err)
project.ComposeFiles = []string{"testdata/publish/compose.yaml"}
service := &composeService{}
layers, err := service.createLayers(t.Context(), project, api.PublishOptions{
WithEnvironment: true,
})
assert.NilError(t, err)
published := string(layers[0].Data)
assert.Equal(t, published, `name: test
services:
test:
extends:
file: f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml
service: foo
string:
image: test
env_file: 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
list:
image: test
env_file:
- 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
mapping:
image: test
env_file:
- path: 5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3.env
`)
expectedLayers := []v1.Descriptor{
{
MediaType: "application/vnd.docker.compose.file+yaml",
Annotations: map[string]string{
"com.docker.compose.file": "compose.yaml",
"com.docker.compose.version": internal.Version,
},
},
{
MediaType: "application/vnd.docker.compose.file+yaml",
Annotations: map[string]string{
"com.docker.compose.extends": "true",
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c",
"com.docker.compose.version": internal.Version,
},
},
{
MediaType: "application/vnd.docker.compose.envfile",
Annotations: map[string]string{
"com.docker.compose.envfile": "5efca9cdbac9f5394c6c2e2094b1b42661f988f57fcab165a0bf72b205451af3",
"com.docker.compose.version": internal.Version,
},
},
}
assert.DeepEqual(t, expectedLayers, layers, cmp.FilterPath(func(path cmp.Path) bool {
return !slices.Contains([]string{".Data", ".Digest", ".Size"}, path.String())
}, cmp.Ignore()))
}
================================================
FILE: pkg/compose/pull.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config/configfile"
clitypes "github.com/docker/cli/cli/config/types"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/client"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/internal/registry"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.pull(ctx, project, options)
}, "pull", s.events)
}
func (s *composeService) pull(ctx context.Context, project *types.Project, opts api.PullOptions) error { //nolint:gocyclo
images, err := s.getLocalImagesDigests(ctx, project)
if err != nil {
return err
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
var (
mustBuild []string
pullErrors = make([]error, len(project.Services))
imagesBeingPulled = map[string]string{}
)
i := 0
for name, service := range project.Services {
if service.Image == "" {
s.events.On(api.Resource{
ID: name,
Status: api.Done,
Text: "Skipped",
Details: "No image to be pulled",
})
continue
}
switch service.PullPolicy {
case types.PullPolicyNever, types.PullPolicyBuild:
s.events.On(api.Resource{
ID: "Image " + service.Image,
Status: api.Done,
Text: "Skipped",
})
continue
case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
if imageAlreadyPresent(service.Image, images) {
s.events.On(api.Resource{
ID: "Image " + service.Image,
Status: api.Done,
Text: "Skipped",
Details: "Image is already present locally",
})
continue
}
}
if service.Build != nil && opts.IgnoreBuildable {
s.events.On(api.Resource{
ID: "Image " + service.Image,
Status: api.Done,
Text: "Skipped",
Details: "Image can be built",
})
continue
}
if _, ok := imagesBeingPulled[service.Image]; ok {
continue
}
imagesBeingPulled[service.Image] = service.Name
idx := i
eg.Go(func() error {
_, err := s.pullServiceImage(ctx, service, opts.Quiet, project.Environment["DOCKER_DEFAULT_PLATFORM"])
if err != nil {
pullErrors[idx] = err
if service.Build != nil {
mustBuild = append(mustBuild, service.Name)
}
if !opts.IgnoreFailures && service.Build == nil {
if s.dryRun {
s.events.On(errorEventf("Image "+service.Image,
"error pulling image: %s", service.Image))
}
// fail fast if image can't be pulled nor built
return err
}
}
return nil
})
i++
}
err = eg.Wait()
if len(mustBuild) > 0 {
logrus.Warnf("WARNING: Some service image(s) must be built from source by running:\n docker compose build %s", strings.Join(mustBuild, " "))
}
if err != nil {
return err
}
if opts.IgnoreFailures {
return nil
}
return errors.Join(pullErrors...)
}
func imageAlreadyPresent(serviceImage string, localImages map[string]api.ImageSummary) bool {
normalizedImage, err := reference.ParseDockerRef(serviceImage)
if err != nil {
return false
}
switch refType := normalizedImage.(type) {
case reference.NamedTagged:
_, ok := localImages[serviceImage]
return ok && refType.Tag() != "latest"
default:
_, ok := localImages[serviceImage]
return ok
}
}
func getUnwrappedErrorMessage(err error) string {
derr := errors.Unwrap(err)
if derr != nil {
return getUnwrappedErrorMessage(derr)
}
return err.Error()
}
func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, quietPull bool, defaultPlatform string) (string, error) {
resource := "Image " + service.Image
s.events.On(pullingEvent(service.Image))
ref, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return "", err
}
encodedAuth, err := encodedAuth(ref, s.configFile())
if err != nil {
return "", err
}
platform := service.Platform
if platform == "" {
platform = defaultPlatform
}
var ociPlatforms []ocispec.Platform
if platform != "" {
p, err := platforms.Parse(platform)
if err != nil {
return "", err
}
ociPlatforms = append(ociPlatforms, p)
}
stream, err := s.apiClient().ImagePull(ctx, service.Image, client.ImagePullOptions{
RegistryAuth: encodedAuth,
Platforms: ociPlatforms,
})
if ctx.Err() != nil {
s.events.On(api.Resource{
ID: resource,
Status: api.Warning,
Text: "Interrupted",
})
return "", nil
}
// check if has error and the service has a build section
// then the status should be warning instead of error
if err != nil && service.Build != nil {
s.events.On(api.Resource{
ID: resource,
Status: api.Warning,
Text: getUnwrappedErrorMessage(err),
})
return "", err
}
if err != nil {
s.events.On(errorEvent(resource, getUnwrappedErrorMessage(err)))
return "", err
}
dec := json.NewDecoder(stream)
for {
var jm jsonstream.Message
if err := dec.Decode(&jm); err != nil {
if errors.Is(err, io.EOF) {
break
}
return "", err
}
if jm.Error != nil {
return "", errors.New(jm.Error.Message)
}
if !quietPull {
toPullProgressEvent(resource, jm, s.events)
}
}
s.events.On(pulledEvent(service.Image))
inspected, err := s.apiClient().ImageInspect(ctx, service.Image)
if err != nil {
return "", err
}
return inspected.ID, nil
}
// ImageDigestResolver creates a func able to resolve image digest from a docker ref,
func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiClient client.APIClient) func(named reference.Named) (digest.Digest, error) {
return func(named reference.Named) (digest.Digest, error) {
auth, err := encodedAuth(named, file)
if err != nil {
return "", err
}
inspect, err := apiClient.DistributionInspect(ctx, named.String(), client.DistributionInspectOptions{
EncodedRegistryAuth: auth,
})
if err != nil {
return "",
fmt.Errorf("failed to resolve digest for %s: %w", named.String(), err)
}
return inspect.Descriptor.Digest, nil
}
}
type authProvider interface {
GetAuthConfig(registryHostname string) (clitypes.AuthConfig, error)
}
func encodedAuth(ref reference.Named, configFile authProvider) (string, error) {
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
if err != nil {
return "", err
}
buf, err := json.Marshal(authConfig)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf), nil
}
func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]api.ImageSummary, quietPull bool) error {
needPull := map[string]types.ServiceConfig{}
for name, service := range project.Services {
pull, err := mustPull(service, images)
if err != nil {
return err
}
if pull {
needPull[name] = service
}
for i, vol := range service.Volumes {
if vol.Type == types.VolumeTypeImage {
if _, ok := images[vol.Source]; !ok {
// Hack: create a fake ServiceConfig so we pull missing volume image
n := fmt.Sprintf("%s:volume %d", name, i)
needPull[n] = types.ServiceConfig{
Name: n,
Image: vol.Source,
}
}
}
}
}
if len(needPull) == 0 {
return nil
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
pulledImages := map[string]api.ImageSummary{}
var mutex sync.Mutex
for name, service := range needPull {
eg.Go(func() error {
id, err := s.pullServiceImage(ctx, service, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
mutex.Lock()
defer mutex.Unlock()
pulledImages[name] = api.ImageSummary{
ID: id,
Repository: service.Image,
LastTagTime: time.Now(),
}
if err != nil && isServiceImageToBuild(service, project.Services) {
// image can be built, so we can ignore pull failure
return nil
}
return err
})
}
err := eg.Wait()
for i, service := range needPull {
if pulledImages[i].ID != "" {
images[service.Image] = pulledImages[i]
}
}
return err
}
func mustPull(service types.ServiceConfig, images map[string]api.ImageSummary) (bool, error) {
if service.Provider != nil {
return false, nil
}
if service.Image == "" {
return false, nil
}
policy, duration, err := service.GetPullPolicy()
if err != nil {
return false, err
}
switch policy {
case types.PullPolicyAlways:
// force pull
return true, nil
case types.PullPolicyNever, types.PullPolicyBuild:
return false, nil
case types.PullPolicyRefresh:
img, ok := images[service.Image]
if !ok {
return true, nil
}
return time.Now().After(img.LastTagTime.Add(duration)), nil
default: // Pull if missing
_, ok := images[service.Image]
return !ok, nil
}
}
func isServiceImageToBuild(service types.ServiceConfig, services types.Services) bool {
if service.Build != nil {
return true
}
if service.Image == "" {
// N.B. this should be impossible as service must have either `build` or `image` (or both)
return false
}
// look through the other services to see if another has a build definition for the same
// image name
for _, svc := range services {
if svc.Image == service.Image && svc.Build != nil {
return true
}
}
return false
}
const (
PreparingPhase = "Preparing"
WaitingPhase = "waiting"
PullingFsPhase = "Pulling fs layer"
DownloadingPhase = "Downloading"
DownloadCompletePhase = "Download complete"
ExtractingPhase = "Extracting"
VerifyingChecksumPhase = "Verifying Checksum"
AlreadyExistsPhase = "Already exists"
PullCompletePhase = "Pull complete"
)
func toPullProgressEvent(parent string, jm jsonstream.Message, events api.EventProcessor) {
if jm.ID == "" || jm.Progress == nil {
return
}
var (
details string
total int64
percent int
current int64
status = api.Working
)
switch jm.Status {
case PreparingPhase, WaitingPhase, PullingFsPhase:
percent = 0
case DownloadingPhase, ExtractingPhase, VerifyingChecksumPhase:
if jm.Progress != nil {
current = jm.Progress.Current
total = jm.Progress.Total
if jm.Progress.Total > 0 {
percent = min(int(jm.Progress.Current*100/jm.Progress.Total), 100)
}
}
case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase:
status = api.Done
percent = 100
}
if strings.Contains(jm.Status, "Image is up to date") ||
strings.Contains(jm.Status, "Downloaded newer image") {
status = api.Done
percent = 100
}
if jm.Error != nil {
status = api.Error
details = jm.Error.Message
} else {
details = units.HumanSize(float64(jm.Progress.Current))
}
events.On(api.Resource{
ID: jm.ID,
ParentID: parent,
Current: current,
Total: total,
Percent: percent,
Status: status,
Text: jm.Status,
Details: details,
})
}
================================================
FILE: pkg/compose/push.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/internal/registry"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
if options.Quiet {
return s.push(ctx, project, options)
}
return Run(ctx, func(ctx context.Context) error {
return s.push(ctx, project, options)
}, "push", s.events)
}
func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
for _, service := range project.Services {
if service.Build == nil || service.Image == "" {
if options.ImageMandatory && service.Image == "" && service.Provider == nil {
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
}
s.events.On(api.Resource{
ID: service.Name,
Status: api.Done,
Text: "Skipped",
})
continue
}
tags := []string{service.Image}
if service.Build != nil {
tags = append(tags, service.Build.Tags...)
}
for _, tag := range tags {
eg.Go(func() error {
s.events.On(newEvent(tag, api.Working, "Pushing"))
err := s.pushServiceImage(ctx, tag, options.Quiet)
if err != nil {
if !options.IgnoreFailures {
s.events.On(newEvent(tag, api.Error, err.Error()))
return err
}
s.events.On(newEvent(tag, api.Warning, err.Error()))
} else {
s.events.On(newEvent(tag, api.Done, "Pushed"))
}
return nil
})
}
}
return eg.Wait()
}
func (s *composeService) pushServiceImage(ctx context.Context, tag string, quietPush bool) error {
ref, err := reference.ParseNormalizedNamed(tag)
if err != nil {
return err
}
authConfig, err := s.configFile().GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
if err != nil {
return err
}
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
stream, err := s.apiClient().ImagePush(ctx, tag, client.ImagePushOptions{
RegistryAuth: base64.URLEncoding.EncodeToString(buf),
})
if err != nil {
return err
}
dec := json.NewDecoder(stream)
for {
var jm jsonstream.Message
if err := dec.Decode(&jm); err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
if jm.Error != nil {
return errors.New(jm.Error.Message)
}
if !quietPush {
toPushProgressEvent(tag, jm, s.events)
}
}
return nil
}
func toPushProgressEvent(prefix string, jm jsonstream.Message, events api.EventProcessor) {
if jm.ID == "" {
// skipped
return
}
var (
text string
status = api.Working
total int64
current int64
percent int
)
if isDone(jm) {
status = api.Done
percent = 100
}
if jm.Error != nil {
status = api.Error
text = jm.Error.Message
}
if jm.Progress != nil {
text = progressText(jm.Progress)
if jm.Progress.Total != 0 {
current = jm.Progress.Current
total = jm.Progress.Total
if jm.Progress.Total > 0 {
percent = min(int(jm.Progress.Current*100/jm.Progress.Total), 100)
}
}
}
events.On(api.Resource{
ParentID: prefix,
ID: jm.ID,
Text: text,
Status: status,
Current: current,
Total: total,
Percent: percent,
})
}
func isDone(msg jsonstream.Message) bool {
// TODO there should be a better way to detect push is done than such a status message check
switch strings.ToLower(msg.Status) {
case "pushed", "layer already exists":
return true
default:
return false
}
}
// progressText is a minimal variant of [jsonmessage.JSONProgress.String()]
//
// [jsonmessage.JSONProgress.String()]: https://github.com/moby/moby/blob/v28.5.2/pkg/jsonmessage/jsonmessage.go#L54-L117
func progressText(p *jsonstream.Progress) string {
switch {
case p.Current <= 0 && p.Total <= 0:
return ""
case p.Units == "": // no units, use bytes
current := units.HumanSize(float64(p.Current))
if p.Total <= 0 || p.Total > p.Current {
// remove total display if the reported current is wonky.
return fmt.Sprintf("%8v", current)
}
total := units.HumanSize(float64(p.Total))
return fmt.Sprintf("%8v/%v", current, total)
default:
if p.Total <= 0 || p.Total > p.Current {
// remove total display if the reported current is wonky.
return fmt.Sprintf("%d %s", p.Current, p.Units)
}
return fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
}
}
================================================
FILE: pkg/compose/remove.go
================================================
/*
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"
"strings"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error {
projectName = strings.ToLower(projectName)
if options.Stop {
err := s.Stop(ctx, projectName, api.StopOptions{
Services: options.Services,
Project: options.Project,
})
if err != nil {
return err
}
}
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, options.Services...)
if err != nil {
if api.IsNotFoundError(err) {
_, _ = fmt.Fprintln(s.stderr(), "No stopped containers")
return nil
}
return err
}
if options.Project != nil {
containers = containers.filter(isService(options.Project.ServiceNames()...))
}
var stoppedContainers Containers
for _, ctr := range containers {
// We have to inspect containers, as State reported by getContainers suffers a race condition
inspected, err := s.apiClient().ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
if api.IsNotFoundError(err) {
// Already removed. Maybe configured with auto-remove
continue
}
if err != nil {
return err
}
if !inspected.Container.State.Running || (options.Stop && s.dryRun) {
stoppedContainers = append(stoppedContainers, ctr)
}
}
var names []string
stoppedContainers.forEach(func(c container.Summary) {
names = append(names, getCanonicalContainerName(c))
})
if len(names) == 0 {
return api.ErrNoResources
}
msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", "))
if options.Force {
_, _ = fmt.Fprintln(s.stdout(), msg)
} else {
confirm, err := s.prompt(msg, false)
if err != nil {
return err
}
if !confirm {
return nil
}
}
return Run(ctx, func(ctx context.Context) error {
return s.remove(ctx, stoppedContainers, options)
}, "remove", s.events)
}
func (s *composeService) remove(ctx context.Context, containers Containers, options api.RemoveOptions) error {
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
eventName := getContainerProgressName(ctr)
s.events.On(removingEvent(eventName))
_, err := s.apiClient().ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{
RemoveVolumes: options.Volumes,
Force: options.Force,
})
if err == nil {
s.events.On(removedEvent(eventName))
}
return err
})
}
return eg.Wait()
}
================================================
FILE: pkg/compose/restart.go
================================================
/*
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"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.restart(ctx, strings.ToLower(projectName), options)
}, "restart", s.events)
}
func (s *composeService) restart(ctx context.Context, projectName string, options api.RestartOptions) error { //nolint:gocyclo
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
if err != nil {
return err
}
project := options.Project
if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil {
return err
}
}
if options.NoDeps {
project, err = project.WithSelectedServices(options.Services, types.IgnoreDependencies)
if err != nil {
return err
}
}
// ignore depends_on relations which are not impacted by restarting service or not required
project, err = project.WithServicesTransform(func(_ string, s types.ServiceConfig) (types.ServiceConfig, error) {
for name, r := range s.DependsOn {
if !r.Restart {
delete(s.DependsOn, name)
}
}
return s, nil
})
if err != nil {
return err
}
if len(options.Services) != 0 {
project, err = project.WithSelectedServices(options.Services, types.IncludeDependents)
if err != nil {
return err
}
}
return InDependencyOrder(ctx, project, func(c context.Context, service string) error {
config := project.Services[service]
err = s.waitDependencies(ctx, project, service, config.DependsOn, containers, 0)
if err != nil {
return err
}
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers.filter(isService(service)) {
eg.Go(func() error {
def := project.Services[service]
for _, hook := range def.PreStop {
err = s.runHook(ctx, ctr, def, hook, nil)
if err != nil {
return err
}
}
eventName := getContainerProgressName(ctr)
s.events.On(restartingEvent(eventName))
_, err = s.apiClient().ContainerRestart(ctx, ctr.ID, client.ContainerRestartOptions{
Timeout: utils.DurationSecondToInt(options.Timeout),
})
if err != nil {
return err
}
s.events.On(startedEvent(eventName))
for _, hook := range def.PostStart {
err = s.runHook(ctx, ctr, def, hook, nil)
if err != nil {
return err
}
}
return nil
})
}
return eg.Wait()
})
}
================================================
FILE: pkg/compose/run.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"slices"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
cmd "github.com/docker/cli/cli/command/container"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/stringid"
"github.com/docker/compose/v5/pkg/api"
)
type prepareRunResult struct {
containerID string
service types.ServiceConfig
created container.Summary
}
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
result, err := s.prepareRun(ctx, project, opts)
if err != nil {
return 0, err
}
// remove cancellable context signal handler so we can forward signals to container without compose from exiting
signal.Reset()
sigc := make(chan os.Signal, 128)
signal.Notify(sigc)
go cmd.ForwardAllSignals(ctx, s.apiClient(), result.containerID, sigc)
defer signal.Stop(sigc)
// If the service has post_start hooks, set up a goroutine that waits for
// the container to start and then executes them. This is needed because
// cmd.RunStart both starts and attaches to the container in one call,
// so we can't run hooks sequentially between start and attach.
var hookErrCh chan error
if len(result.service.PostStart) > 0 {
hookErrCh = make(chan error, 1)
go func() {
hookErrCh <- s.runPostStartHooksOnEvent(ctx, result.containerID, result.service, result.created)
}()
}
err = cmd.RunStart(ctx, s.dockerCli, &cmd.StartOptions{
OpenStdin: !opts.Detach && opts.Interactive,
Attach: !opts.Detach,
Containers: []string{result.containerID},
DetachKeys: s.configFile().DetachKeys,
})
// Wait for hooks to complete if they were started
if hookErrCh != nil {
if hookErr := <-hookErrCh; hookErr != nil && err == nil {
err = hookErr
}
}
var stErr cli.StatusError
if errors.As(err, &stErr) {
return stErr.StatusCode, nil
}
return 0, err
}
// runPostStartHooksOnEvent listens for the container's start event and executes
// post_start lifecycle hooks once the container is running.
func (s *composeService) runPostStartHooksOnEvent(ctx context.Context, containerID string, service types.ServiceConfig, ctr container.Summary) error {
evtCtx, cancel := context.WithCancel(ctx)
defer cancel()
res := s.apiClient().Events(evtCtx, client.EventsListOptions{
Filters: make(client.Filters).
Add("type", "container").
Add("container", containerID).
Add("event", string(events.ActionStart)),
})
// Wait for the container start event
select {
case <-evtCtx.Done():
return evtCtx.Err()
case err := <-res.Err:
return err
case <-res.Messages:
// Container started, run hooks
}
for _, hook := range service.PostStart {
if err := s.runHook(ctx, ctr, service, hook, nil); err != nil {
return err
}
}
return nil
}
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (prepareRunResult, error) {
// Temporary implementation of use_api_socket until we get actual support inside docker engine
project, err := s.useAPISocket(project)
if err != nil {
return prepareRunResult{}, err
}
err = Run(ctx, func(ctx context.Context) error {
return s.startDependencies(ctx, project, opts)
}, "run", s.events)
if err != nil {
return prepareRunResult{}, err
}
service, err := project.GetService(opts.Service)
if err != nil {
return prepareRunResult{}, err
}
applyRunOptions(project, &service, opts)
if err := s.stdin().CheckTty(opts.Interactive, service.Tty); err != nil {
return prepareRunResult{}, err
}
slug := stringid.GenerateRandomID()
if service.ContainerName == "" {
service.ContainerName = fmt.Sprintf("%[1]s%[4]s%[2]s%[4]srun%[4]s%[3]s", project.Name, service.Name, stringid.TruncateID(slug), api.Separator)
}
one := 1
service.Scale = &one
service.Restart = ""
if service.Deploy != nil {
service.Deploy.RestartPolicy = nil
}
service.CustomLabels = service.CustomLabels.
Add(api.SlugLabel, slug).
Add(api.OneoffLabel, "True")
// Only ensure image exists for the target service, dependencies were already handled by startDependencies
buildOpts := prepareBuildOptions(opts)
if err := s.ensureImagesExists(ctx, project, buildOpts, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
return prepareRunResult{}, err
}
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
if err != nil {
return prepareRunResult{}, err
}
if !opts.NoDeps {
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
return prepareRunResult{}, err
}
}
createOpts := createOptions{
AutoRemove: opts.AutoRemove,
AttachStdin: opts.Interactive,
UseNetworkAliases: opts.UseNetworkAliases,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
err = newConvergence(project.ServiceNames(), observedState, nil, nil, s).resolveServiceReferences(&service)
if err != nil {
return prepareRunResult{}, err
}
err = s.ensureModels(ctx, project, opts.QuietPull)
if err != nil {
return prepareRunResult{}, err
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
if err != nil {
return prepareRunResult{}, err
}
inspect, err := s.apiClient().ContainerInspect(ctx, created.ID, client.ContainerInspectOptions{})
if err != nil {
return prepareRunResult{}, err
}
err = s.injectSecrets(ctx, project, service, inspect.Container.ID)
if err != nil {
return prepareRunResult{containerID: created.ID}, err
}
err = s.injectConfigs(ctx, project, service, inspect.Container.ID)
return prepareRunResult{
containerID: created.ID,
service: service,
created: created,
}, err
}
func prepareBuildOptions(opts api.RunOptions) *api.BuildOptions {
if opts.Build == nil {
return nil
}
// Create a copy of build options and restrict to only the target service
buildOptsCopy := *opts.Build
buildOptsCopy.Services = []string{opts.Service}
return &buildOptsCopy
}
func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
service.Tty = opts.Tty
service.StdinOpen = opts.Interactive
service.ContainerName = opts.Name
if len(opts.Command) > 0 {
service.Command = opts.Command
}
if opts.User != "" {
service.User = opts.User
}
if len(opts.CapAdd) > 0 {
service.CapAdd = append(service.CapAdd, opts.CapAdd...)
service.CapDrop = slices.DeleteFunc(service.CapDrop, func(e string) bool { return slices.Contains(opts.CapAdd, e) })
}
if len(opts.CapDrop) > 0 {
service.CapDrop = append(service.CapDrop, opts.CapDrop...)
service.CapAdd = slices.DeleteFunc(service.CapAdd, func(e string) bool { return slices.Contains(opts.CapDrop, e) })
}
if opts.WorkingDir != "" {
service.WorkingDir = opts.WorkingDir
}
if opts.Entrypoint != nil {
service.Entrypoint = opts.Entrypoint
if len(opts.Command) == 0 {
service.Command = []string{}
}
}
if len(opts.Environment) > 0 {
cmdEnv := types.NewMappingWithEquals(opts.Environment)
serviceOverrideEnv := cmdEnv.Resolve(func(s string) (string, bool) {
v, ok := envResolver(project.Environment)(s)
return v, ok
}).RemoveEmpty()
if service.Environment == nil {
service.Environment = types.MappingWithEquals{}
}
service.Environment.OverrideBy(serviceOverrideEnv)
}
for k, v := range opts.Labels {
service.Labels = service.Labels.Add(k, v)
}
}
func (s *composeService) startDependencies(ctx context.Context, project *types.Project, options api.RunOptions) error {
project = project.WithServicesDisabled(options.Service)
err := s.Create(ctx, project, api.CreateOptions{
Build: options.Build,
IgnoreOrphans: options.IgnoreOrphans,
RemoveOrphans: options.RemoveOrphans,
QuietPull: options.QuietPull,
})
if err != nil {
return err
}
if len(project.Services) > 0 {
return s.Start(ctx, project.Name, api.StartOptions{
Project: project,
})
}
return nil
}
================================================
FILE: pkg/compose/scale.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
return Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
err := s.create(ctx, project, api.CreateOptions{Services: options.Services})
if err != nil {
return err
}
return s.start(ctx, project.Name, api.StartOptions{Project: project, Services: options.Services}, nil)
}), "scale", s.events)
}
================================================
FILE: pkg/compose/secrets.go
================================================
/*
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 (
"archive/tar"
"bytes"
"context"
"fmt"
"strconv"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/client"
)
type mountType string
const (
secretMount mountType = "secret"
configMount mountType = "config"
)
func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
return s.injectFileReferences(ctx, project, service, id, secretMount)
}
func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
return s.injectFileReferences(ctx, project, service, id, configMount)
}
func (s *composeService) injectFileReferences(ctx context.Context, project *types.Project, service types.ServiceConfig, id string, mountType mountType) error {
mounts, sources := s.getFilesAndMap(project, service, mountType)
for _, mount := range mounts {
content, err := s.resolveFileContent(project, sources[mount.Source], mountType)
if err != nil {
return err
}
if content == "" {
continue
}
if service.ReadOnly {
return fmt.Errorf("cannot create %s %q in read-only service %s: `file` is the sole supported option", mountType, sources[mount.Source].Name, service.Name)
}
s.setDefaultTarget(&mount, mountType)
if err := s.copyFileToContainer(ctx, id, content, mount); err != nil {
return err
}
}
return nil
}
func (s *composeService) getFilesAndMap(project *types.Project, service types.ServiceConfig, mountType mountType) ([]types.FileReferenceConfig, map[string]types.FileObjectConfig) {
var files []types.FileReferenceConfig
var fileMap map[string]types.FileObjectConfig
switch mountType {
case secretMount:
files = make([]types.FileReferenceConfig, len(service.Secrets))
for i, config := range service.Secrets {
files[i] = types.FileReferenceConfig(config)
}
fileMap = make(map[string]types.FileObjectConfig)
for k, v := range project.Secrets {
fileMap[k] = types.FileObjectConfig(v)
}
case configMount:
files = make([]types.FileReferenceConfig, len(service.Configs))
for i, config := range service.Configs {
files[i] = types.FileReferenceConfig(config)
}
fileMap = make(map[string]types.FileObjectConfig)
for k, v := range project.Configs {
fileMap[k] = types.FileObjectConfig(v)
}
}
return files, fileMap
}
func (s *composeService) resolveFileContent(project *types.Project, source types.FileObjectConfig, mountType mountType) (string, error) {
if source.Content != "" {
// inlined, or already resolved by include
return source.Content, nil
}
if source.Environment != "" {
env, ok := project.Environment[source.Environment]
if !ok {
return "", fmt.Errorf("environment variable %q required by %s %q is not set", source.Environment, mountType, source.Name)
}
return env, nil
}
return "", nil
}
func (s *composeService) setDefaultTarget(file *types.FileReferenceConfig, mountType mountType) {
if file.Target == "" {
if mountType == secretMount {
file.Target = "/run/secrets/" + file.Source
} else {
file.Target = "/" + file.Source
}
} else if mountType == secretMount && !isAbsTarget(file.Target) {
file.Target = "/run/secrets/" + file.Target
}
}
func (s *composeService) copyFileToContainer(ctx context.Context, id, content string, file types.FileReferenceConfig) error {
b, err := createTar(content, file)
if err != nil {
return err
}
_, err = s.apiClient().CopyToContainer(ctx, id, client.CopyToContainerOptions{
DestinationPath: "/",
Content: &b,
CopyUIDGID: file.UID != "" || file.GID != "",
})
return err
}
func createTar(env string, config types.FileReferenceConfig) (bytes.Buffer, error) {
value := []byte(env)
b := bytes.Buffer{}
tarWriter := tar.NewWriter(&b)
mode := types.FileMode(0o444)
if config.Mode != nil {
mode = *config.Mode
}
var uid, gid int
if config.UID != "" {
v, err := strconv.Atoi(config.UID)
if err != nil {
return b, err
}
uid = v
}
if config.GID != "" {
v, err := strconv.Atoi(config.GID)
if err != nil {
return b, err
}
gid = v
}
header := &tar.Header{
Name: config.Target,
Size: int64(len(value)),
Mode: int64(mode),
ModTime: time.Now(),
Uid: uid,
Gid: gid,
}
err := tarWriter.WriteHeader(header)
if err != nil {
return bytes.Buffer{}, err
}
_, err = tarWriter.Write(value)
if err != nil {
return bytes.Buffer{}, err
}
err = tarWriter.Close()
return b, err
}
================================================
FILE: pkg/compose/shellout.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os"
"os/exec"
"path/filepath"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"github.com/docker/compose/v5/internal"
)
// prepareShellOut prepare a shell-out command to be ran by Compose
func (s *composeService) prepareShellOut(gctx context.Context, env types.Mapping, cmd *exec.Cmd) error {
env = env.Clone()
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
delete(env, metadata.ReexecEnvvar)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(gctx, &carrier)
env.Merge(types.Mapping(carrier))
cmd.Env = env.Values()
return nil
}
// propagateDockerEndpoint produces DOCKER_* env vars for a child CLI plugin to target the same docker endpoint
// `cleanup` func MUST be called after child process completion to enforce removal of cert files
func (s *composeService) propagateDockerEndpoint() ([]string, func(), error) {
cleanup := func() {}
env := types.Mapping{}
env[command.EnvOverrideContext] = s.dockerCli.CurrentContext()
env["USER_AGENT"] = "compose/" + internal.Version
endpoint := s.dockerCli.DockerEndpoint()
env[client.EnvOverrideHost] = endpoint.Host
if endpoint.TLSData != nil {
certs, err := os.MkdirTemp("", "compose")
if err != nil {
return nil, cleanup, err
}
cleanup = func() {
_ = os.RemoveAll(certs)
}
env[client.EnvOverrideCertPath] = certs
env["DOCKER_TLS"] = "1"
if !endpoint.SkipTLSVerify {
env[client.EnvTLSVerify] = "1"
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultKeyFile), endpoint.TLSData.Key, 0o600)
if err != nil {
return nil, cleanup, err
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultCertFile), endpoint.TLSData.Cert, 0o600)
if err != nil {
return nil, cleanup, err
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultCaFile), endpoint.TLSData.CA, 0o600)
if err != nil {
return nil, cleanup, err
}
}
return env.Values(), cleanup, nil
}
================================================
FILE: pkg/compose/start.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.start(ctx, strings.ToLower(projectName), options, nil)
}, "start", s.events)
}
func (s *composeService) start(ctx context.Context, projectName string, options api.StartOptions, listener api.ContainerEventListener) error {
project := options.Project
if project == nil {
var containers Containers
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
if err != nil {
return err
}
project, err = s.projectFromName(containers, projectName, options.AttachTo...)
if err != nil {
return err
}
}
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: projectFilter(project.Name).Add("label", oneOffFilter(false)),
All: true,
})
if err != nil {
return err
}
containers := Containers(res.Items)
err = InDependencyOrder(ctx, project, func(c context.Context, name string) error {
service, err := project.GetService(name)
if err != nil {
return err
}
return s.startService(ctx, project, service, containers, listener, options.WaitTimeout)
})
if err != nil {
return err
}
if options.Wait {
depends := types.DependsOnConfig{}
for _, s := range project.Services {
depends[s.Name] = types.ServiceDependency{
Condition: getDependencyCondition(s, project),
Required: true,
}
}
if options.WaitTimeout > 0 {
withTimeout, cancel := context.WithTimeout(ctx, options.WaitTimeout)
ctx = withTimeout
defer cancel()
}
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)
}
return err
}
}
return nil
}
// getDependencyCondition checks if service is depended on by other services
// with service_completed_successfully condition, and applies that condition
// instead, or --wait will never finish waiting for one-shot containers
func getDependencyCondition(service types.ServiceConfig, project *types.Project) string {
for _, services := range project.Services {
for dependencyService, dependencyConfig := range services.DependsOn {
if dependencyService == service.Name && dependencyConfig.Condition == types.ServiceConditionCompletedSuccessfully {
return types.ServiceConditionCompletedSuccessfully
}
}
}
return ServiceConditionRunningOrHealthy
}
================================================
FILE: pkg/compose/stop.go
================================================
/*
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"
"slices"
"strings"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
return Run(ctx, func(ctx context.Context) error {
return s.stop(ctx, strings.ToLower(projectName), options, nil)
}, "stop", s.events)
}
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions, event api.ContainerEventListener) error {
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
if err != nil {
return err
}
project := options.Project
if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil {
return err
}
}
if len(options.Services) == 0 {
options.Services = project.ServiceNames()
}
return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
if !slices.Contains(options.Services, service) {
return nil
}
serv := project.Services[service]
return s.stopContainers(ctx, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout, event)
})
}
================================================
FILE: pkg/compose/stop_test.go
================================================
/*
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 (
"strings"
"testing"
"time"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
func TestStopTimeout(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service1", "456", false),
testContainer("service2", "789", false),
},
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
client.VolumeListOptions{
Filters: projectFilter(strings.ToLower(testProject)),
}).
Return(client.VolumeListResult{}, nil)
api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
Return(client.NetworkListResult{}, nil)
timeout := 2 * time.Second
stopConfig := client.ContainerStopOptions{Timeout: utils.DurationSecondToInt(&timeout)}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopConfig).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "456", stopConfig).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", stopConfig).Return(client.ContainerStopResult{}, nil)
err = tested.Stop(t.Context(), strings.ToLower(testProject), compose.StopOptions{
Timeout: &timeout,
})
assert.NilError(t, err)
}
================================================
FILE: pkg/compose/suffix_unix.go
================================================
//go:build !windows
/*
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
func executable(s string) string {
return s
}
================================================
FILE: pkg/compose/testdata/compose.yaml
================================================
services:
service1:
image: nginx
service2:
image: mysql
================================================
FILE: pkg/compose/testdata/publish/common.yaml
================================================
services:
foo:
image: bar
================================================
FILE: pkg/compose/testdata/publish/compose.yaml
================================================
name: test
services:
test:
extends:
file: common.yaml
service: foo
string:
image: test
env_file: test.env
list:
image: test
env_file:
- test.env
mapping:
image: test
env_file:
- path: test.env
================================================
FILE: pkg/compose/testdata/publish/test.env
================================================
HELLO=WORLD
================================================
FILE: pkg/compose/top.go
================================================
/*
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"
"strings"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Top(ctx context.Context, projectName string, services []string) ([]api.ContainerProcSummary, error) {
projectName = strings.ToLower(projectName)
var containers Containers
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false)
if err != nil {
return nil, err
}
if len(services) > 0 {
containers = containers.filter(isService(services...))
}
summary := make([]api.ContainerProcSummary, len(containers))
eg, ctx := errgroup.WithContext(ctx)
for i, ctr := range containers {
eg.Go(func() error {
topContent, err := s.apiClient().ContainerTop(ctx, ctr.ID, client.ContainerTopOptions{
Arguments: []string{},
})
if err != nil {
return err
}
name := getCanonicalContainerName(ctr)
s := api.ContainerProcSummary{
ID: ctr.ID,
Name: name,
Processes: topContent.Processes,
Titles: topContent.Titles,
Service: name,
}
if service, exists := ctr.Labels[api.ServiceLabel]; exists {
s.Service = service
}
if replica, exists := ctr.Labels[api.ContainerNumberLabel]; exists {
s.Replica = replica
}
summary[i] = s
return nil
})
}
return summary, eg.Wait()
}
================================================
FILE: pkg/compose/transform/replace.go
================================================
/*
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 transform
import (
"fmt"
"go.yaml.in/yaml/v4"
)
// ReplaceExtendsFile changes value for service.extends.file in input yaml stream, preserving formatting
func ReplaceExtendsFile(in []byte, service string, value string) ([]byte, error) {
var doc yaml.Node
err := yaml.Unmarshal(in, &doc)
if err != nil {
return nil, err
}
if doc.Kind != yaml.DocumentNode {
return nil, fmt.Errorf("expected document kind %v, got %v", yaml.DocumentNode, doc.Kind)
}
root := doc.Content[0]
if root.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expected document root to be a mapping, got %v", root.Kind)
}
services, err := getMapping(root, "services")
if err != nil {
return nil, err
}
target, err := getMapping(services, service)
if err != nil {
return nil, err
}
extends, err := getMapping(target, "extends")
if err != nil {
return nil, err
}
file, err := getMapping(extends, "file")
if err != nil {
return nil, err
}
// we've found target `file` yaml node. Let's replace value in stream at node position
return replace(in, file.Line, file.Column, value), nil
}
// ReplaceEnvFile changes value for service.extends.env_file in input yaml stream, preserving formatting
func ReplaceEnvFile(in []byte, service string, i int, value string) ([]byte, error) {
var doc yaml.Node
err := yaml.Unmarshal(in, &doc)
if err != nil {
return nil, err
}
if doc.Kind != yaml.DocumentNode {
return nil, fmt.Errorf("expected document kind %v, got %v", yaml.DocumentNode, doc.Kind)
}
root := doc.Content[0]
if root.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expected document root to be a mapping, got %v", root.Kind)
}
services, err := getMapping(root, "services")
if err != nil {
return nil, err
}
target, err := getMapping(services, service)
if err != nil {
return nil, err
}
envFile, err := getMapping(target, "env_file")
if err != nil {
return nil, err
}
// env_file can be either a string, sequence of strings, or sequence of mappings with path attribute
if envFile.Kind == yaml.SequenceNode {
envFile = envFile.Content[i]
if envFile.Kind == yaml.MappingNode {
envFile, err = getMapping(envFile, "path")
if err != nil {
return nil, err
}
}
return replace(in, envFile.Line, envFile.Column, value), nil
} else {
return replace(in, envFile.Line, envFile.Column, value), nil
}
}
func getMapping(root *yaml.Node, key string) (*yaml.Node, error) {
var node *yaml.Node
l := len(root.Content)
for i := 0; i < l; i += 2 {
k := root.Content[i]
if k.Kind != yaml.ScalarNode || k.Tag != "!!str" {
return nil, fmt.Errorf("expected mapping key to be a string, got %v %v", root.Kind, k.Tag)
}
if k.Value == key {
node = root.Content[i+1]
return node, nil
}
}
return nil, fmt.Errorf("key %v not found", key)
}
// replace changes yaml node value in stream at position, preserving content
func replace(in []byte, line int, column int, value string) []byte {
var out []byte
l := 1
pos := 0
for _, b := range in {
if b == '\n' {
l++
if l == line {
break
}
}
pos++
}
pos += column
out = append(out, in[0:pos]...)
out = append(out, []byte(value)...)
for ; pos < len(in); pos++ {
if in[pos] == '\n' {
break
}
}
out = append(out, in[pos:]...)
return out
}
================================================
FILE: pkg/compose/transform/replace_test.go
================================================
/*
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 transform
import (
"reflect"
"testing"
"gotest.tools/v3/assert"
)
func TestReplace(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "simple",
in: `services:
test:
extends:
file: foo.yaml
service: foo
`,
want: `services:
test:
extends:
file: REPLACED
service: foo
`,
},
{
name: "last line",
in: `services:
test:
extends:
service: foo
file: foo.yaml
`,
want: `services:
test:
extends:
service: foo
file: REPLACED
`,
},
{
name: "last line no CR",
in: `services:
test:
extends:
service: foo
file: foo.yaml`,
want: `services:
test:
extends:
service: foo
file: REPLACED`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ReplaceExtendsFile([]byte(tt.in), "test", "REPLACED")
assert.NilError(t, err)
if !reflect.DeepEqual(got, []byte(tt.want)) {
t.Errorf("ReplaceExtendsFile() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: pkg/compose/up.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"slices"
"sync"
"sync/atomic"
"syscall"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/eiannone/keyboard"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
err := s.create(ctx, project, options.Create)
if err != nil {
return err
}
if options.Start.Attach == nil {
return s.start(ctx, project.Name, options.Start, nil)
}
return nil
}), "up", s.events)
if err != nil {
return err
}
if options.Start.Attach == nil {
return err
}
if s.dryRun {
_, _ = fmt.Fprintln(s.stdout(), "end of 'compose up' output, interactive run is not supported in dry-run mode")
return err
}
// if we get a second signal during shutdown, we kill the services
// immediately, so the channel needs to have sufficient capacity or
// we might miss a signal while setting up the second channel read
// (this is also why signal.Notify is used vs signal.NotifyContext)
signalChan := make(chan os.Signal, 2)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(signalChan)
var isTerminated atomic.Bool
var (
logConsumer = options.Start.Attach
navigationMenu *formatter.LogKeyboard
kEvents <-chan keyboard.KeyEvent
)
if options.Start.NavigationMenu {
kEvents, err = keyboard.GetKeys(100)
if err != nil {
logrus.Warnf("could not start menu, an error occurred while starting: %v", err)
options.Start.NavigationMenu = false
} else {
defer keyboard.Close() //nolint:errcheck
isDockerDesktopActive, err := s.isDesktopIntegrationActive(ctx)
if err != nil {
return err
}
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive)
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan)
logConsumer = navigationMenu.Decorate(logConsumer)
}
}
watcher, err := NewWatcher(project, options, s.watch, logConsumer)
if err != nil && options.Start.Watch {
return err
}
if navigationMenu != nil && watcher != nil {
navigationMenu.EnableWatch(options.Start.Watch, watcher)
}
printer := newLogPrinter(logConsumer)
// global context to handle canceling goroutines
globalCtx, cancel := context.WithCancel(ctx)
defer cancel()
if navigationMenu != nil {
navigationMenu.EnableDetach(cancel)
}
var (
eg errgroup.Group
mu sync.Mutex
errs []error
)
appendErr := func(err error) {
if err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
}
eg.Go(func() error {
first := true
gracefulTeardown := func() {
first = false
s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Gracefully Stopping... press Ctrl+C again to force"))
eg.Go(func() error {
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
}, printer.HandleEvent)
appendErr(err)
return nil
})
isTerminated.Store(true)
}
for {
select {
case <-globalCtx.Done():
if watcher != nil {
return watcher.Stop()
}
return nil
case <-ctx.Done():
if first {
gracefulTeardown()
}
case <-signalChan:
if first {
_ = keyboard.Close()
gracefulTeardown()
break
}
eg.Go(func() error {
err := s.kill(context.WithoutCancel(globalCtx), project.Name, api.KillOptions{
Services: options.Create.Services,
Project: project,
All: true,
})
// Ignore errors indicating that some of the containers were already stopped or removed.
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) || errors.Is(err, api.ErrNoResources) {
return nil
}
appendErr(err)
return nil
})
return nil
case event := <-kEvents:
navigationMenu.HandleKeyEvents(globalCtx, event, project, options)
}
}
})
if options.Start.Watch && watcher != nil {
if err := watcher.Start(globalCtx); err != nil {
// cancel the global context to terminate background goroutines
cancel()
_ = eg.Wait()
return err
}
}
monitor := newMonitor(s.apiClient(), project.Name)
if len(options.Start.Services) > 0 {
monitor.withServices(options.Start.Services)
} else {
// Start.AttachTo have been already curated with only the services to monitor
monitor.withServices(options.Start.AttachTo)
}
monitor.withListener(printer.HandleEvent)
var exitCode int
if options.Start.OnExit != api.CascadeIgnore {
once := true
// detect first container to exit to trigger application shutdown
monitor.withListener(func(event api.ContainerEvent) {
if once && event.Type == api.ContainerEventExited {
if options.Start.OnExit == api.CascadeFail && event.ExitCode == 0 {
return
}
once = false
exitCode = event.ExitCode
s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Aborting on container exit..."))
eg.Go(func() error {
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
}, printer.HandleEvent)
appendErr(err)
return nil
})
}
})
}
if options.Start.ExitCodeFrom != "" {
once := true
// capture exit code from first container to exit with selected service
monitor.withListener(func(event api.ContainerEvent) {
if once && event.Type == api.ContainerEventExited && event.Service == options.Start.ExitCodeFrom {
exitCode = event.ExitCode
once = false
}
})
}
containers, err := s.attach(globalCtx, project, printer.HandleEvent, options.Start.AttachTo)
if err != nil {
cancel()
_ = eg.Wait()
return err
}
attached := make([]string, len(containers))
for i, ctr := range containers {
attached[i] = ctr.ID
}
monitor.withListener(func(event api.ContainerEvent) {
if event.Type != api.ContainerEventStarted {
return
}
if slices.Contains(attached, event.ID) && !event.Restarting {
return
}
eg.Go(func() error {
res, err := s.apiClient().ContainerInspect(globalCtx, event.ID, client.ContainerInspectOptions{})
if err != nil {
appendErr(err)
return nil
}
err = s.doLogContainer(globalCtx, options.Start.Attach, event.Source, res.Container, api.LogOptions{
Follow: true,
Since: res.Container.State.StartedAt,
})
if errdefs.IsNotImplemented(err) {
// container may be configured with logging_driver: none
// as container already started, we might miss the very first logs. But still better than none
err := s.doAttachContainer(globalCtx, event.Service, event.ID, event.Source, printer.HandleEvent)
appendErr(err)
return nil
}
appendErr(err)
return nil
})
})
eg.Go(func() error {
err := monitor.Start(globalCtx)
// cancel the global context to terminate signal-handler goroutines
cancel()
appendErr(err)
return nil
})
// We use the parent context without cancellation as we manage sigterm to stop the stack
err = s.start(context.WithoutCancel(ctx), project.Name, options.Start, printer.HandleEvent)
if err != nil && !isTerminated.Load() { // Ignore error if the process is terminated
cancel()
_ = eg.Wait()
return err
}
_ = eg.Wait()
err = errors.Join(errs...)
if exitCode != 0 {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
}
return err
}
================================================
FILE: pkg/compose/viz.go
================================================
/*
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"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v5/pkg/api"
)
// maps a service with the services it depends on
type vizGraph map[*types.ServiceConfig][]*types.ServiceConfig
func (s *composeService) Viz(_ context.Context, project *types.Project, opts api.VizOptions) (string, error) {
graph := make(vizGraph)
for _, service := range project.Services {
graph[&service] = make([]*types.ServiceConfig, 0, len(service.DependsOn))
for dependencyName := range service.DependsOn {
// no error should be returned since dependencyName should exist
dependency, _ := project.GetService(dependencyName)
graph[&service] = append(graph[&service], &dependency)
}
}
// build graphviz graph
var graphBuilder strings.Builder
// graph name
graphBuilder.WriteString("digraph ")
writeQuoted(&graphBuilder, project.Name)
graphBuilder.WriteString(" {\n")
// graph layout
// dot is the perfect layout for this use case since graph is directed and hierarchical
graphBuilder.WriteString(opts.Indentation + "layout=dot;\n")
addNodes(&graphBuilder, graph, project.Name, &opts)
graphBuilder.WriteByte('\n')
addEdges(&graphBuilder, graph, &opts)
graphBuilder.WriteString("}\n")
return graphBuilder.String(), nil
}
// addNodes adds the corresponding graphviz representation of all the nodes in the given graph to the graphBuilder
// returns the same graphBuilder
func addNodes(graphBuilder *strings.Builder, graph vizGraph, projectName string, opts *api.VizOptions) *strings.Builder {
for serviceNode := range graph {
// write:
// "service name" [style="filled" label<service name
graphBuilder.WriteString(opts.Indentation)
writeQuoted(graphBuilder, serviceNode.Name)
graphBuilder.WriteString(" [style=\"filled\" label=<")
graphBuilder.WriteString(serviceNode.Name)
graphBuilder.WriteString("")
if opts.IncludeNetworks && len(serviceNode.Networks) > 0 {
graphBuilder.WriteString("")
graphBuilder.WriteString("
Networks:")
for _, networkName := range serviceNode.NetworksByPriority() {
graphBuilder.WriteString("
")
graphBuilder.WriteString(networkName)
}
graphBuilder.WriteString("")
}
if opts.IncludePorts && len(serviceNode.Ports) > 0 {
graphBuilder.WriteString("")
graphBuilder.WriteString("
Ports:")
for _, portConfig := range serviceNode.Ports {
graphBuilder.WriteString("
")
if portConfig.HostIP != "" {
graphBuilder.WriteString(portConfig.HostIP)
graphBuilder.WriteByte(':')
}
graphBuilder.WriteString(portConfig.Published)
graphBuilder.WriteByte(':')
graphBuilder.WriteString(strconv.Itoa(int(portConfig.Target)))
graphBuilder.WriteString(" (")
graphBuilder.WriteString(portConfig.Protocol)
graphBuilder.WriteString(", ")
graphBuilder.WriteString(portConfig.Mode)
graphBuilder.WriteString(")")
}
graphBuilder.WriteString("")
}
if opts.IncludeImageName {
graphBuilder.WriteString("")
graphBuilder.WriteString("
Image:
")
graphBuilder.WriteString(api.GetImageNameOrDefault(*serviceNode, projectName))
graphBuilder.WriteString("")
}
graphBuilder.WriteString(">];\n")
}
return graphBuilder
}
// addEdges adds the corresponding graphviz representation of all edges in the given graph to the graphBuilder
// returns the same graphBuilder
func addEdges(graphBuilder *strings.Builder, graph vizGraph, opts *api.VizOptions) *strings.Builder {
for parent, children := range graph {
for _, child := range children {
graphBuilder.WriteString(opts.Indentation)
writeQuoted(graphBuilder, parent.Name)
graphBuilder.WriteString(" -> ")
writeQuoted(graphBuilder, child.Name)
graphBuilder.WriteString(";\n")
}
}
return graphBuilder
}
// writeQuoted writes "str" to builder
func writeQuoted(builder *strings.Builder, str string) {
builder.WriteByte('"')
builder.WriteString(str)
builder.WriteByte('"')
}
================================================
FILE: pkg/compose/viz_test.go
================================================
/*
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 (
"strconv"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
compose "github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestViz(t *testing.T) {
project := types.Project{
Name: "viz-test",
WorkingDir: "/home",
Services: types.Services{
"service1": {
Name: "service1",
Image: "image-for-service1",
Ports: []types.ServicePortConfig{
{
Published: "80",
Target: 80,
Protocol: "tcp",
},
{
Published: "53",
Target: 533,
Protocol: "udp",
},
},
Networks: map[string]*types.ServiceNetworkConfig{
"internal": nil,
},
},
"service2": {
Name: "service2",
Image: "image-for-service2",
Ports: []types.ServicePortConfig{},
},
"service3": {
Name: "service3",
Image: "some-image",
DependsOn: map[string]types.ServiceDependency{
"service2": {},
"service1": {},
},
},
"service4": {
Name: "service4",
Image: "another-image",
DependsOn: map[string]types.ServiceDependency{
"service3": {},
},
Ports: []types.ServicePortConfig{
{
Published: "8080",
Target: 80,
},
},
Networks: map[string]*types.ServiceNetworkConfig{
"external": nil,
},
},
"With host IP": {
Name: "With host IP",
Image: "user/image-name",
DependsOn: map[string]types.ServiceDependency{
"service1": {},
},
Ports: []types.ServicePortConfig{
{
Published: "8888",
Target: 8080,
HostIP: "127.0.0.1",
},
},
},
},
Networks: types.Networks{
"internal": types.NetworkConfig{},
"external": types.NetworkConfig{},
"not-used": types.NetworkConfig{},
},
Volumes: nil,
Secrets: nil,
Configs: nil,
Extensions: nil,
ComposeFiles: nil,
Environment: nil,
DisabledServices: nil,
Profiles: nil,
}
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
require.NoError(t, err)
t.Run("viz (no ports, networks or image)", func(t *testing.T) {
graphStr, err := tested.Viz(t.Context(), &project, compose.VizOptions{
Indentation: " ",
IncludePorts: false,
IncludeImageName: false,
IncludeNetworks: false,
})
require.NoError(t, err, "viz command failed")
// check indentation
assert.Contains(t, graphStr, "\n ", graphStr)
assert.NotContains(t, graphStr, "\n ", graphStr)
// check digraph name
assert.Contains(t, graphStr, "digraph \""+project.Name+"\"", graphStr)
// check nodes
for _, service := range project.Services {
assert.Contains(t, graphStr, "\""+service.Name+"\" [style=\"filled\"", graphStr)
}
// check node attributes
assert.NotContains(t, graphStr, "Networks", graphStr)
assert.NotContains(t, graphStr, "Image", graphStr)
assert.NotContains(t, graphStr, "Ports", graphStr)
// check edges that SHOULD exist in the generated graph
allowedEdges := make(map[string][]string)
for name, service := range project.Services {
allowed := make([]string, 0, len(service.DependsOn))
for depName := range service.DependsOn {
allowed = append(allowed, depName)
}
allowedEdges[name] = allowed
}
for serviceName, dependencies := range allowedEdges {
for _, dependencyName := range dependencies {
assert.Contains(t, graphStr, "\""+serviceName+"\" -> \""+dependencyName+"\"", graphStr)
}
}
// check edges that SHOULD NOT exist in the generated graph
forbiddenEdges := make(map[string][]string)
for name, service := range project.Services {
forbiddenEdges[name] = make([]string, 0, len(project.ServiceNames())-len(service.DependsOn))
for _, serviceName := range project.ServiceNames() {
_, edgeExists := service.DependsOn[serviceName]
if !edgeExists {
forbiddenEdges[name] = append(forbiddenEdges[name], serviceName)
}
}
}
for serviceName, forbiddenDeps := range forbiddenEdges {
for _, forbiddenDep := range forbiddenDeps {
assert.NotContains(t, graphStr, "\""+serviceName+"\" -> \""+forbiddenDep+"\"")
}
}
})
t.Run("viz (with ports, networks and image)", func(t *testing.T) {
graphStr, err := tested.Viz(t.Context(), &project, compose.VizOptions{
Indentation: "\t",
IncludePorts: true,
IncludeImageName: true,
IncludeNetworks: true,
})
require.NoError(t, err, "viz command failed")
// check indentation
assert.Contains(t, graphStr, "\n\t", graphStr)
assert.NotContains(t, graphStr, "\n\t\t", graphStr)
// check digraph name
assert.Contains(t, graphStr, "digraph \""+project.Name+"\"", graphStr)
// check nodes
for _, service := range project.Services {
assert.Contains(t, graphStr, "\""+service.Name+"\" [style=\"filled\"", graphStr)
}
// check node attributes
assert.Contains(t, graphStr, "Networks", graphStr)
assert.Contains(t, graphStr, ">internal<", graphStr)
assert.Contains(t, graphStr, ">external<", graphStr)
assert.Contains(t, graphStr, "Image", graphStr)
for _, service := range project.Services {
assert.Contains(t, graphStr, ">"+service.Image+"<", graphStr)
}
assert.Contains(t, graphStr, "Ports", graphStr)
for _, service := range project.Services {
for _, portConfig := range service.Ports {
assert.NotContains(t, graphStr, ">"+portConfig.Published+":"+strconv.Itoa(int(portConfig.Target))+"<", graphStr)
}
}
})
}
================================================
FILE: pkg/compose/volumes.go
================================================
/*
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"
"slices"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Volumes(ctx context.Context, project string, options api.VolumesOptions) ([]api.VolumesSummary, error) {
allContainers, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: projectFilter(project),
})
if err != nil {
return nil, err
}
var containers []container.Summary
if len(options.Services) > 0 {
// filter service containers
for _, c := range allContainers.Items {
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
containers = append(containers, c)
}
}
} else {
containers = allContainers.Items
}
volumesResponse, err := s.apiClient().VolumeList(ctx, client.VolumeListOptions{
Filters: projectFilter(project),
})
if err != nil {
return nil, err
}
projectVolumes := volumesResponse.Items
if len(options.Services) == 0 {
return projectVolumes, nil
}
var volumes []api.VolumesSummary
// create a name lookup of volumes used by containers
serviceVolumes := make(map[string]bool)
for _, ctr := range containers {
for _, mount := range ctr.Mounts {
serviceVolumes[mount.Name] = true
}
}
// append if volumes in this project are in serviceVolumes
for _, v := range projectVolumes {
if serviceVolumes[v.Name] {
volumes = append(volumes, v)
}
}
return volumes, nil
}
================================================
FILE: pkg/compose/volumes_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestVolumes(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockApi, mockCli := prepareMocks(mockCtrl)
tested := composeService{
dockerCli: mockCli,
}
// Create test volumes
vol1 := volume.Volume{Name: testProject + "_vol1"}
vol2 := volume.Volume{Name: testProject + "_vol2"}
vol3 := volume.Volume{Name: testProject + "_vol3"}
// Create test containers with volume mounts
c1 := container.Summary{
Labels: map[string]string{api.ServiceLabel: "service1"},
Mounts: []container.MountPoint{
{Name: testProject + "_vol1"},
{Name: testProject + "_vol2"},
},
}
c2 := container.Summary{
Labels: map[string]string{api.ServiceLabel: "service2"},
Mounts: []container.MountPoint{
{Name: testProject + "_vol3"},
},
}
listOpts := client.ContainerListOptions{Filters: projectFilter(testProject)}
volumeListOpts := client.VolumeListOptions{Filters: projectFilter(testProject)}
volumeReturn := client.VolumeListResult{
Items: []volume.Volume{vol1, vol2, vol3},
}
containerReturn := client.ContainerListResult{
Items: []container.Summary{c1, c2},
}
mockApi.EXPECT().ContainerList(t.Context(), listOpts).Times(2).Return(containerReturn, nil)
mockApi.EXPECT().VolumeList(t.Context(), volumeListOpts).Times(2).Return(volumeReturn, nil)
// Test without service filter - should return all project volumes
volumes, err := tested.Volumes(t.Context(), testProject, api.VolumesOptions{})
expected := []api.VolumesSummary{vol1, vol2, vol3}
assert.NilError(t, err)
assert.DeepEqual(t, volumes, expected)
// Test with service filter - should only return volumes used by service1
volumes, err = tested.Volumes(t.Context(), testProject, api.VolumesOptions{Services: []string{"service1"}})
expected = []api.VolumesSummary{vol1, vol2}
assert.NilError(t, err)
assert.DeepEqual(t, volumes, expected)
}
================================================
FILE: pkg/compose/wait.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...)
if err != nil {
return 0, err
}
if len(containers) == 0 {
return 0, fmt.Errorf("no containers for project %q", projectName)
}
eg, waitCtx := errgroup.WithContext(ctx)
var statusCode int64
for _, ctr := range containers {
eg.Go(func() error {
var err error
res := s.apiClient().ContainerWait(waitCtx, ctr.ID, client.ContainerWaitOptions{})
select {
case result := <-res.Result:
_, _ = fmt.Fprintf(s.stdout(), "container %q exited with status code %d\n", ctr.ID, result.StatusCode)
statusCode = result.StatusCode
case err = <-res.Error:
}
return err
})
}
err = eg.Wait()
if err != nil {
return 42, err // Ignore abort flag in case of error in wait
}
if options.DownProjectOnContainerExit {
return statusCode, s.Down(ctx, projectName, api.DownOptions{
RemoveOrphans: true,
})
}
return statusCode, err
}
================================================
FILE: pkg/compose/watch.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
gsync "sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils"
ccli "github.com/docker/cli/cli/command/container"
"github.com/go-viper/mapstructure/v2"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
pathutil "github.com/docker/compose/v5/internal/paths"
"github.com/docker/compose/v5/internal/sync"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
cutils "github.com/docker/compose/v5/pkg/utils"
"github.com/docker/compose/v5/pkg/watch"
)
type WatchFunc func(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error)
type Watcher struct {
project *types.Project
options api.WatchOptions
watchFn WatchFunc
stopFn func()
errCh chan error
}
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc, consumer api.LogConsumer) (*Watcher, error) {
for i := range project.Services {
service := project.Services[i]
if service.Develop != nil && service.Develop.Watch != nil {
build := options.Create.Build
return &Watcher{
project: project,
options: api.WatchOptions{
LogTo: consumer,
Build: build,
},
watchFn: w,
errCh: make(chan error),
}, nil
}
}
// none of the services is eligible to watch
return nil, fmt.Errorf("none of the selected services is configured for watch, see https://docs.docker.com/compose/how-tos/file-watch/")
}
// ensure state changes are atomic
var mx gsync.Mutex
func (w *Watcher) Start(ctx context.Context) error {
mx.Lock()
defer mx.Unlock()
ctx, cancelFunc := context.WithCancel(ctx)
w.stopFn = cancelFunc
wait, err := w.watchFn(ctx, w.project, w.options)
if err != nil {
go func() {
w.errCh <- err
}()
return err
}
go func() {
w.errCh <- wait()
}()
return nil
}
func (w *Watcher) Stop() error {
mx.Lock()
defer mx.Unlock()
if w.stopFn == nil {
return nil
}
w.stopFn()
w.stopFn = nil
err := <-w.errCh
return err
}
// getSyncImplementation returns an appropriate sync implementation for the
// project.
//
// Currently, an implementation that batches files and transfers them using
// the Moby `Untar` API.
func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syncer, error) {
var useTar bool
if useTarEnv, ok := os.LookupEnv("COMPOSE_EXPERIMENTAL_WATCH_TAR"); ok {
useTar, _ = strconv.ParseBool(useTarEnv)
} else {
useTar = true
}
if !useTar {
return nil, errors.New("no available sync implementation")
}
return sync.NewTar(project.Name, tarDockerClient{s: s}), nil
}
func (s *composeService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
wait, err := s.watch(ctx, project, options)
if err != nil {
return err
}
return wait()
}
type watchRule struct {
types.Trigger
include watch.PathMatcher
ignore watch.PathMatcher
service string
}
func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
hostPath := string(event)
if !pathutil.IsChild(r.Path, hostPath) {
return nil
}
included, err := r.include.Matches(hostPath)
if err != nil {
logrus.Warnf("error include matching %q: %v", hostPath, err)
return nil
}
if !included {
logrus.Debugf("%s is not matching include pattern", hostPath)
return nil
}
isIgnored, err := r.ignore.Matches(hostPath)
if err != nil {
logrus.Warnf("error ignore matching %q: %v", hostPath, err)
return nil
}
if isIgnored {
logrus.Debugf("%s is matching ignore pattern", hostPath)
return nil
}
var containerPath string
if r.Target != "" {
rel, err := filepath.Rel(r.Path, hostPath)
if err != nil {
logrus.Warnf("error making %s relative to %s: %v", hostPath, r.Path, err)
return nil
}
// always use Unix-style paths for inside the container
containerPath = path.Join(r.Target, filepath.ToSlash(rel))
}
return &sync.PathMapping{
HostPath: hostPath,
ContainerPath: containerPath,
}
}
func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) { //nolint: gocyclo
var err error
if project, err = project.WithSelectedServices(options.Services); err != nil {
return nil, err
}
syncer, err := s.getSyncImplementation(project)
if err != nil {
return nil, err
}
eg, ctx := errgroup.WithContext(ctx)
var (
rules []watchRule
paths []string
)
for serviceName, service := range project.Services {
config, err := loadDevelopmentConfig(service, project)
if err != nil {
return nil, err
}
if service.Develop != nil {
config = service.Develop
}
if config == nil {
continue
}
for _, trigger := range config.Watch {
if trigger.Action == types.WatchActionRebuild {
if service.Build == nil {
return nil, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
}
if options.Build == nil {
return nil, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
}
// set the service to always be built - watch triggers `Up()` when it receives a rebuild event
service.PullPolicy = types.PullPolicyBuild
project.Services[serviceName] = service
}
}
for _, trigger := range config.Watch {
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
continue
} else {
shouldInitialSync := trigger.InitialSync
// Check legacy extension attribute for backward compatibility
if !shouldInitialSync {
var legacyInitialSync bool
success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync)
if err == nil && success && legacyInitialSync {
shouldInitialSync = true
logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n")
}
}
if shouldInitialSync && isSync(trigger) {
// Need to check initial files are in container that are meant to be synced from watch action
err := s.initialSync(ctx, project, service, trigger, syncer)
if err != nil {
return nil, err
}
}
}
paths = append(paths, trigger.Path)
}
serviceWatchRules, err := getWatchRules(config, service)
if err != nil {
return nil, err
}
rules = append(rules, serviceWatchRules...)
}
if len(paths) == 0 {
return nil, fmt.Errorf("none of the selected services is configured for watch, consider setting a 'develop' section")
}
watcher, err := watch.NewWatcher(paths)
if err != nil {
return nil, err
}
err = watcher.Start()
if err != nil {
return nil, err
}
eg.Go(func() error {
return s.watchEvents(ctx, project, options, watcher, syncer, rules)
})
options.LogTo.Log(api.WatchLogger, "Watch enabled")
return func() error {
err := eg.Wait()
if werr := watcher.Close(); werr != nil {
logrus.Debugf("Error closing Watcher: %v", werr)
}
return err
}, nil
}
func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) {
var rules []watchRule
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
if err != nil {
return nil, err
}
// add a hardcoded set of ignores on top of what came from .dockerignore
// some of this should likely be configurable (e.g. there could be cases
// where you want `.git` to be synced) but this is suitable for now
dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
if err != nil {
return nil, err
}
for _, trigger := range config.Watch {
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
if err != nil {
return nil, err
}
var include watch.PathMatcher
if len(trigger.Include) == 0 {
include = watch.AnyMatcher{}
} else {
include, err = watch.NewDockerPatternMatcher(trigger.Path, trigger.Include)
if err != nil {
return nil, err
}
}
rules = append(rules, watchRule{
Trigger: trigger,
include: include,
ignore: watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
ignore,
),
service: service.Name,
})
}
return rules, nil
}
func isSync(trigger types.Trigger) bool {
return trigger.Action == types.WatchActionSync || trigger.Action == types.WatchActionSyncRestart
}
func (s *composeService) watchEvents(ctx context.Context, project *types.Project, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, rules []watchRule) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// debounce and group filesystem events so that we capture IDE saving many files as one "batch" event
batchEvents := watch.BatchDebounceEvents(ctx, s.clock, watcher.Events())
for {
select {
case <-ctx.Done():
options.LogTo.Log(api.WatchLogger, "Watch disabled")
// Ensure watcher is closed to release resources
_ = watcher.Close()
return nil
case err, open := <-watcher.Errors():
if err != nil {
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors: "+err.Error())
}
if open {
continue
}
_ = watcher.Close()
return err
case batch, ok := <-batchEvents:
if !ok {
options.LogTo.Log(api.WatchLogger, "Watch disabled")
_ = watcher.Close()
return nil
}
if len(batch) > 1000 {
logrus.Warnf("Very large batch of file changes detected: %d files. This may impact performance.", len(batch))
options.LogTo.Log(api.WatchLogger, "Large batch of file changes detected. If you just switched branches, this is expected.")
}
start := time.Now()
logrus.Debugf("batch start: count[%d]", len(batch))
err := s.handleWatchBatch(ctx, project, options, batch, rules, syncer)
if err != nil {
logrus.Warnf("Error handling changed files: %v", err)
// If context was canceled, exit immediately
if ctx.Err() != nil {
_ = watcher.Close()
return ctx.Err()
}
}
logrus.Debugf("batch complete: duration[%s] count[%d]", time.Since(start), len(batch))
}
}
}
func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (*types.DevelopConfig, error) {
var config types.DevelopConfig
y, ok := service.Extensions["x-develop"]
if !ok {
return nil, nil
}
logrus.Warnf("x-develop is DEPRECATED, please use the official `develop` attribute")
err := mapstructure.Decode(y, &config)
if err != nil {
return nil, err
}
baseDir, err := filepath.EvalSymlinks(project.WorkingDir)
if err != nil {
return nil, fmt.Errorf("resolving symlink for %q: %w", project.WorkingDir, err)
}
for i, trigger := range config.Watch {
if !filepath.IsAbs(trigger.Path) {
trigger.Path = filepath.Join(baseDir, trigger.Path)
}
if p, err := filepath.EvalSymlinks(trigger.Path); err == nil {
// this might fail because the path doesn't exist, etc.
trigger.Path = p
}
trigger.Path = filepath.Clean(trigger.Path)
if trigger.Path == "" {
return nil, errors.New("watch rules MUST define a path")
}
if trigger.Action == types.WatchActionRebuild && service.Build == nil {
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name)
}
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
return nil, fmt.Errorf("can't watch with action %q on service %s without a command", types.WatchActionSyncExec, service.Name)
}
config.Watch[i] = trigger
}
return &config, nil
}
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
for _, volume := range volumes {
if volume.Bind != nil {
relPath, err := filepath.Rel(volume.Source, watchPath)
if err == nil && !strings.HasPrefix(relPath, "..") {
return true
}
}
}
return false
}
type tarDockerClient struct {
s *composeService
}
func (t tarDockerClient) ContainersForService(ctx context.Context, projectName string, serviceName string) ([]container.Summary, error) {
containers, err := t.s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
if err != nil {
return nil, err
}
return containers, nil
}
func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error {
execCreateResp, err := t.s.apiClient().ExecCreate(ctx, containerID, client.ExecCreateOptions{
Cmd: cmd,
AttachStdout: false,
AttachStderr: true,
AttachStdin: in != nil,
TTY: false,
})
if err != nil {
return err
}
conn, err := t.s.apiClient().ExecAttach(ctx, execCreateResp.ID, client.ExecAttachOptions{
TTY: false,
})
if err != nil {
return err
}
defer conn.Close()
var eg errgroup.Group
if in != nil {
eg.Go(func() error {
defer func() {
_ = conn.CloseWrite()
}()
_, err := io.Copy(conn.Conn, in)
return err
})
}
eg.Go(func() error {
_, err := io.Copy(t.s.stdout(), conn.Reader)
return err
})
_, err = t.s.apiClient().ExecStart(ctx, execCreateResp.ID, client.ExecStartOptions{
TTY: false,
Detach: false,
})
if err != nil {
return err
}
// although the errgroup is not tied directly to the context, the operations
// in it are reading/writing to the connection, which is tied to the context,
// so they won't block indefinitely
if err := eg.Wait(); err != nil {
return err
}
execResult, err := t.s.apiClient().ExecInspect(ctx, execCreateResp.ID, client.ExecInspectOptions{})
if err != nil {
return err
}
if execResult.Running {
return errors.New("process still running")
}
if execResult.ExitCode != 0 {
return fmt.Errorf("exit code %d", execResult.ExitCode)
}
return nil
}
func (t tarDockerClient) Untar(ctx context.Context, id string, archive io.ReadCloser) error {
_, err := t.s.apiClient().CopyToContainer(ctx, id, client.CopyToContainerOptions{
DestinationPath: "/",
Content: archive,
CopyUIDGID: true,
})
return err
}
//nolint:gocyclo
func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, options api.WatchOptions, batch []watch.FileEvent, rules []watchRule, syncer sync.Syncer) error {
var (
restart = map[string]bool{}
syncfiles = map[string][]*sync.PathMapping{}
exec = map[string][]int{}
rebuild = map[string]bool{}
)
for _, event := range batch {
for i, rule := range rules {
mapping := rule.Matches(event)
if mapping == nil {
continue
}
switch rule.Action {
case types.WatchActionRebuild:
rebuild[rule.service] = true
case types.WatchActionSync:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
case types.WatchActionRestart:
restart[rule.service] = true
case types.WatchActionSyncRestart:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
restart[rule.service] = true
case types.WatchActionSyncExec:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
// We want to run exec hooks only once after syncfiles if multiple file events match
// as we can't compare ServiceHook to sort and compact a slice, collect rule indexes
exec[rule.service] = append(exec[rule.service], i)
}
}
}
logrus.Debugf("watch actions: rebuild %d sync %d restart %d", len(rebuild), len(syncfiles), len(restart))
if len(rebuild) > 0 {
err := s.rebuild(ctx, project, utils.MapKeys(rebuild), options)
if err != nil {
return err
}
}
for serviceName, pathMappings := range syncfiles {
writeWatchSyncMessage(options.LogTo, serviceName, pathMappings)
err := syncer.Sync(ctx, serviceName, pathMappings)
if err != nil {
return err
}
}
if len(restart) > 0 {
services := utils.MapKeys(restart)
err := s.restart(ctx, project.Name, api.RestartOptions{
Services: services,
Project: project,
NoDeps: false,
})
if err != nil {
return err
}
options.LogTo.Log(
api.WatchLogger,
fmt.Sprintf("service(s) %q restarted", services))
}
eg, ctx := errgroup.WithContext(ctx)
for service, rulesToExec := range exec {
slices.Sort(rulesToExec)
for _, i := range slices.Compact(rulesToExec) {
err := s.exec(ctx, project, service, rules[i].Exec, eg)
if err != nil {
return err
}
}
}
return eg.Wait()
}
func (s *composeService) exec(ctx context.Context, project *types.Project, serviceName string, x types.ServiceHook, eg *errgroup.Group) error {
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, serviceName)
if err != nil {
return err
}
for _, c := range containers {
eg.Go(func() error {
exec := ccli.NewExecOptions()
exec.User = x.User
exec.Privileged = x.Privileged
exec.Command = x.Command
exec.Workdir = x.WorkingDir
exec.DetachKeys = s.configFile().DetachKeys
for _, v := range x.Environment.ToMapping().Values() {
err := exec.Env.Set(v)
if err != nil {
return err
}
}
return ccli.RunExec(ctx, s.dockerCli, c.ID, exec)
})
}
return nil
}
func (s *composeService) rebuild(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
// restrict the build to ONLY this service, not any of its dependencies
options.Build.Services = services
options.Build.Progress = string(progressui.PlainMode)
options.Build.Out = cutils.GetWriter(func(line string) {
options.LogTo.Log(api.WatchLogger, line)
})
var (
imageNameToIdMap map[string]string
err error
)
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
imageNameToIdMap, err = s.build(ctx, project, *options.Build, nil)
return err
})(ctx)
if err != nil {
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
return err
}
if options.Prune {
s.pruneDanglingImagesOnRebuild(ctx, project.Name, imageNameToIdMap)
}
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service(s) %q successfully built", services))
err = s.create(ctx, project, api.CreateOptions{
Services: services,
Inherit: true,
Recreate: api.RecreateForce,
})
if err != nil {
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Failed to recreate services after update. Error: %v", err))
return err
}
p, err := project.WithSelectedServices(services, types.IncludeDependents)
if err != nil {
return err
}
err = s.start(ctx, project.Name, api.StartOptions{
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))
}
return nil
}
// writeWatchSyncMessage prints out a message about the sync for the changed paths.
func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []*sync.PathMapping) {
if logrus.IsLevelEnabled(logrus.DebugLevel) {
hostPathsToSync := make([]string, len(pathMappings))
for i := range pathMappings {
hostPathsToSync[i] = pathMappings[i].HostPath
}
log.Log(
api.WatchLogger,
fmt.Sprintf(
"Syncing service %q after changes were detected: %s",
serviceName,
strings.Join(hostPathsToSync, ", "),
),
)
} else {
log.Log(
api.WatchLogger,
fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings)),
)
}
}
func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, projectName string, imageNameToIdMap map[string]string) {
images, err := s.apiClient().ImageList(ctx, client.ImageListOptions{
Filters: projectFilter(projectName).Add("dangling", "true"),
})
if err != nil {
logrus.Debugf("Failed to list images: %v", err)
return
}
for _, img := range images.Items {
if _, ok := imageNameToIdMap[img.ID]; !ok {
_, err := s.apiClient().ImageRemove(ctx, img.ID, client.ImageRemoveOptions{})
if err != nil {
logrus.Debugf("Failed to remove image %s: %v", img.ID, err)
}
}
}
}
// Walks develop.watch.path and checks which files should be copied inside the container
// ignores develop.watch.ignore, Dockerfile, compose files, bind mounted paths and .git
func (s *composeService) initialSync(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, syncer sync.Syncer) error {
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
if err != nil {
return err
}
dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
if err != nil {
return err
}
triggerIgnore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
if err != nil {
return err
}
// FIXME .dockerignore
ignoreInitialSync := watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
triggerIgnore)
pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
if err != nil {
return err
}
return syncer.Sync(ctx, service.Name, pathsToCopy)
}
// Syncs files from develop.watch.path if thy have been modified after the image has been created
//
//nolint:gocyclo
func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) {
fi, err := os.Stat(trigger.Path)
if err != nil {
return nil, err
}
timeImageCreated, err := s.imageCreatedTime(ctx, project, service.Name)
if err != nil {
return nil, err
}
var pathsToCopy []*sync.PathMapping
switch mode := fi.Mode(); {
case mode.IsDir():
// process directory
err = filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// handle possible path err, just in case...
return err
}
if trigger.Path == path {
// walk starts at the root directory
return nil
}
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
// By definition sync ignores bind mounted paths
if d.IsDir() {
// skip folder
return fs.SkipDir
}
return nil // skip file
}
info, err := d.Info()
if err != nil {
return err
}
if !d.IsDir() {
if info.ModTime().Before(timeImageCreated) {
// skip file if it was modified before image creation
return nil
}
rel, err := filepath.Rel(trigger.Path, path)
if err != nil {
return err
}
// only copy files (and not full directories)
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: path,
ContainerPath: filepath.Join(trigger.Target, rel),
})
}
return nil
})
case mode.IsRegular():
// process file
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: trigger.Path,
ContainerPath: trigger.Target,
})
}
}
return pathsToCopy, err
}
func shouldIgnore(name string, ignore watch.PathMatcher) bool {
shouldIgnore, _ := ignore.Matches(name)
// ignore files that match any ignore pattern
return shouldIgnore
}
// gets the image creation time for a service
func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Project, serviceName string) (time.Time, error) {
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: projectFilter(project.Name).Add("label", serviceFilter(serviceName)),
})
if err != nil {
return time.Now(), err
}
if len(res.Items) == 0 {
return time.Now(), fmt.Errorf("could not get created time for service's image")
}
img, err := s.apiClient().ImageInspect(ctx, res.Items[0].ImageID)
if err != nil {
return time.Now(), err
}
// Need to get the oldest one?
timeCreated, err := time.Parse(time.RFC3339Nano, img.Created)
if err != nil {
return time.Now(), err
}
return timeCreated, nil
}
================================================
FILE: pkg/compose/watch_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/streams"
"github.com/jonboulle/clockwork"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/internal/sync"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
"github.com/docker/compose/v5/pkg/watch"
)
type testWatcher struct {
events chan watch.FileEvent
errors chan error
}
func (t testWatcher) Start() error {
return nil
}
func (t testWatcher) Close() error {
return nil
}
func (t testWatcher) Events() chan watch.FileEvent {
return t.events
}
func (t testWatcher) Errors() chan error {
return t.errors
}
type stdLogger struct{}
func (s stdLogger) Log(containerName, message string) {
fmt.Printf("%s: %s\n", containerName, message)
}
func (s stdLogger) Err(containerName, message string) {
fmt.Fprintf(os.Stderr, "%s: %s\n", containerName, message)
}
func (s stdLogger) Status(containerName, msg string) {
fmt.Printf("%s: %s\n", containerName, msg)
}
func TestWatch_Sync(t *testing.T) {
mockCtrl := gomock.NewController(t)
cli := mocks.NewMockCli(mockCtrl)
cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
apiClient := mocks.NewMockAPIClient(mockCtrl)
apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return(client.ContainerListResult{
Items: []container.Summary{
testContainer("test", "123", false),
},
}, nil).AnyTimes()
// we expect the image to be pruned
apiClient.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{
Filters: make(client.Filters).
Add("dangling", "true").
Add("label", api.ProjectLabel+"=myProjectName"),
}).Return(client.ImageListResult{
Items: []image.Summary{
{ID: "123"},
{ID: "456"},
},
}, nil).Times(1)
apiClient.EXPECT().ImageRemove(gomock.Any(), "123", client.ImageRemoveOptions{}).Times(1)
apiClient.EXPECT().ImageRemove(gomock.Any(), "456", client.ImageRemoveOptions{}).Times(1)
//
cli.EXPECT().Client().Return(apiClient).AnyTimes()
ctx, cancelFunc := context.WithCancel(t.Context())
t.Cleanup(cancelFunc)
proj := types.Project{
Name: "myProjectName",
Services: types.Services{
"test": {
Name: "test",
},
},
}
watcher := testWatcher{
events: make(chan watch.FileEvent),
errors: make(chan error),
}
syncer := newFakeSyncer()
clock := clockwork.NewFakeClock()
go func() {
service := composeService{
dockerCli: cli,
clock: clock,
}
rules, err := getWatchRules(&types.DevelopConfig{
Watch: []types.Trigger{
{
Path: "/sync",
Action: "sync",
Target: "/work",
Ignore: []string{"ignore"},
},
{
Path: "/rebuild",
Action: "rebuild",
},
},
}, types.ServiceConfig{Name: "test"})
assert.NilError(t, err)
err = service.watchEvents(ctx, &proj, api.WatchOptions{
Build: &api.BuildOptions{},
LogTo: stdLogger{},
Prune: true,
}, watcher, syncer, rules)
assert.NilError(t, err)
}()
watcher.Events() <- watch.NewFileEvent("/sync/changed")
watcher.Events() <- watch.NewFileEvent("/sync/changed/sub")
err := clock.BlockUntilContext(ctx, 3)
assert.NilError(t, err)
clock.Advance(watch.QuietPeriod)
select {
case actual := <-syncer.synced:
require.ElementsMatch(t, []*sync.PathMapping{
{HostPath: "/sync/changed", ContainerPath: "/work/changed"},
{HostPath: "/sync/changed/sub", ContainerPath: "/work/changed/sub"},
}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}
watcher.Events() <- watch.NewFileEvent("/rebuild")
watcher.Events() <- watch.NewFileEvent("/sync/changed")
err = clock.BlockUntilContext(ctx, 4)
assert.NilError(t, err)
clock.Advance(watch.QuietPeriod)
select {
case batch := <-syncer.synced:
t.Fatalf("received unexpected events: %v", batch)
case <-time.After(100 * time.Millisecond):
// expected
}
// TODO: there's not a great way to assert that the rebuild attempt happened
}
type fakeSyncer struct {
synced chan []*sync.PathMapping
}
func newFakeSyncer() *fakeSyncer {
return &fakeSyncer{
synced: make(chan []*sync.PathMapping),
}
}
func (f *fakeSyncer) Sync(ctx context.Context, service string, paths []*sync.PathMapping) error {
f.synced <- paths
return nil
}
================================================
FILE: pkg/dryrun/dryrunclient.go
================================================
/*
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 dryrun
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"runtime"
"strings"
"sync"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli/command"
containerType "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
)
var _ client.APIClient = &DryRunClient{}
// DryRunClient implements APIClient by delegating to implementation functions. This allows lazy init and per-method overrides
type DryRunClient struct {
apiClient client.APIClient
containers []containerType.Summary
execs sync.Map
resolver *imagetools.Resolver
}
type execDetails struct {
container string
command []string
}
type fakeStreamResult struct {
io.ReadCloser
client.ImagePushResponse // same interface as [client.ImagePullResponse]
}
func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) }
func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() }
// NewDryRunClient produces a DryRunClient
func NewDryRunClient(apiClient client.APIClient, cli command.Cli) (*DryRunClient, error) {
b, err := builder.New(cli, builder.WithSkippedValidation())
if err != nil {
return nil, err
}
configFile, err := b.ImageOpt()
if err != nil {
return nil, err
}
return &DryRunClient{
apiClient: apiClient,
containers: []containerType.Summary{},
execs: sync.Map{},
resolver: imagetools.New(configFile),
}, nil
}
func getCallingFunction() string {
pc, _, _, _ := runtime.Caller(2)
fullName := runtime.FuncForPC(pc).Name()
return fullName[strings.LastIndex(fullName, ".")+1:]
}
// All methods and functions which need to be overridden for dry run.
func (d *DryRunClient) ContainerAttach(ctx context.Context, container string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
return client.ContainerAttachResult{}, errors.New("interactive run is not supported in dry-run mode")
}
func (d *DryRunClient) ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
d.containers = append(d.containers, containerType.Summary{
ID: options.Name,
Names: []string{options.Name},
Labels: options.Config.Labels,
HostConfig: struct {
NetworkMode string `json:",omitempty"`
Annotations map[string]string `json:",omitempty"`
}{},
})
return client.ContainerCreateResult{ID: options.Name}, nil
}
func (d *DryRunClient) ContainerInspect(ctx context.Context, container string, options client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
containerJSON, err := d.apiClient.ContainerInspect(ctx, container, options)
if err != nil {
id := "dryRunId"
for _, c := range d.containers {
if c.ID == container {
id = container
}
}
return client.ContainerInspectResult{
Container: containerType.InspectResponse{
ID: id,
Name: container,
State: &containerType.State{
Status: containerType.StateRunning, // needed for --wait option
Health: &containerType.Health{
Status: containerType.Healthy, // needed for healthcheck control
},
},
Mounts: nil,
Config: &containerType.Config{},
NetworkSettings: &containerType.NetworkSettings{},
},
}, nil
}
return containerJSON, err
}
func (d *DryRunClient) ContainerKill(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
return client.ContainerKillResult{}, nil
}
func (d *DryRunClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
caller := getCallingFunction()
switch caller {
case "start":
return client.ContainerListResult{
Items: d.containers,
}, nil
case "getContainers":
if len(d.containers) == 0 {
res, err := d.apiClient.ContainerList(ctx, options)
if err == nil {
d.containers = res.Items
}
return client.ContainerListResult{
Items: d.containers,
}, err
}
}
return d.apiClient.ContainerList(ctx, options)
}
func (d *DryRunClient) ContainerPause(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
return client.ContainerPauseResult{}, nil
}
func (d *DryRunClient) ContainerRemove(ctx context.Context, container string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
return client.ContainerRemoveResult{}, nil
}
func (d *DryRunClient) ContainerRename(ctx context.Context, container string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
return client.ContainerRenameResult{}, nil
}
func (d *DryRunClient) ContainerRestart(ctx context.Context, container string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
return client.ContainerRestartResult{}, nil
}
func (d *DryRunClient) ContainerStart(ctx context.Context, container string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
return client.ContainerStartResult{}, nil
}
func (d *DryRunClient) ContainerStop(ctx context.Context, container string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
return client.ContainerStopResult{}, nil
}
func (d *DryRunClient) ContainerUnpause(ctx context.Context, container string, options client.ContainerUnpauseOptions) (client.ContainerUnpauseResult, error) {
return client.ContainerUnpauseResult{}, nil
}
func (d *DryRunClient) CopyFromContainer(ctx context.Context, container string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
if _, err := d.ContainerStatPath(ctx, container, client.ContainerStatPathOptions{Path: options.SourcePath}); err != nil {
return client.CopyFromContainerResult{}, fmt.Errorf("could not find the file %s in container %s", options.SourcePath, container)
}
return client.CopyFromContainerResult{}, nil
}
func (d *DryRunClient) CopyToContainer(ctx context.Context, container string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
return client.CopyToContainerResult{}, nil
}
func (d *DryRunClient) ImageBuild(ctx context.Context, reader io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
return client.ImageBuildResult{
Body: io.NopCloser(bytes.NewReader(nil)),
}, nil
}
func (d *DryRunClient) ImageInspect(ctx context.Context, imageName string, options ...client.ImageInspectOption) (client.ImageInspectResult, error) {
caller := getCallingFunction()
switch caller {
case "pullServiceImage", "buildContainerVolumes":
return client.ImageInspectResult{
InspectResponse: image.InspectResponse{ID: "dryRunId"},
}, nil
default:
return d.apiClient.ImageInspect(ctx, imageName, options...)
}
}
func (d *DryRunClient) ImagePull(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
if _, _, err := d.resolver.Resolve(ctx, ref); err != nil {
return nil, err
}
return fakeStreamResult{ReadCloser: http.NoBody}, nil
}
func (d *DryRunClient) ImagePush(ctx context.Context, ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) {
if _, _, err := d.resolver.Resolve(ctx, ref); err != nil {
return nil, err
}
jsonMessage, err := json.Marshal(&jsonstream.Message{
Status: "Pushed",
Progress: &jsonstream.Progress{
Current: 100,
Total: 100,
Start: 0,
HideCounts: false,
Units: "Mb",
},
ID: ref,
})
if err != nil {
return nil, err
}
return fakeStreamResult{ReadCloser: io.NopCloser(bytes.NewReader(jsonMessage))}, nil
}
func (d *DryRunClient) ImageRemove(ctx context.Context, imageName string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) {
return client.ImageRemoveResult{}, nil
}
func (d *DryRunClient) NetworkConnect(ctx context.Context, networkName string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) {
return client.NetworkConnectResult{}, nil
}
func (d *DryRunClient) NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
return client.NetworkCreateResult{
ID: name,
}, nil
}
func (d *DryRunClient) NetworkDisconnect(ctx context.Context, networkName string, options client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) {
return client.NetworkDisconnectResult{}, nil
}
func (d *DryRunClient) NetworkRemove(ctx context.Context, networkName string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
return client.NetworkRemoveResult{}, nil
}
func (d *DryRunClient) VolumeCreate(ctx context.Context, options client.VolumeCreateOptions) (client.VolumeCreateResult, error) {
return client.VolumeCreateResult{
Volume: volume.Volume{
ClusterVolume: nil,
Driver: options.Driver,
Labels: options.Labels,
Name: options.Name,
Options: options.DriverOpts,
},
}, nil
}
func (d *DryRunClient) VolumeRemove(ctx context.Context, volumeID string, options client.VolumeRemoveOptions) (client.VolumeRemoveResult, error) {
return client.VolumeRemoveResult{}, nil
}
func (d *DryRunClient) ExecCreate(ctx context.Context, container string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
b := make([]byte, 32)
_, _ = rand.Read(b)
id := fmt.Sprintf("%x", b)
d.execs.Store(id, execDetails{
container: container,
command: config.Cmd,
})
return client.ExecCreateResult{
ID: id,
}, nil
}
func (d *DryRunClient) ExecStart(ctx context.Context, execID string, config client.ExecStartOptions) (client.ExecStartResult, error) {
_, ok := d.execs.LoadAndDelete(execID)
if !ok {
return client.ExecStartResult{}, fmt.Errorf("invalid exec ID %q", execID)
}
return client.ExecStartResult{}, nil
}
// Functions delegated to original APIClient (not used by Compose or not modifying the Compose stack)
func (d *DryRunClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return d.apiClient.ConfigList(ctx, options)
}
func (d *DryRunClient) ConfigInspect(ctx context.Context, name string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
return d.apiClient.ConfigInspect(ctx, name, options)
}
func (d *DryRunClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
return d.apiClient.ConfigCreate(ctx, options)
}
func (d *DryRunClient) ConfigRemove(ctx context.Context, id string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
return d.apiClient.ConfigRemove(ctx, id, options)
}
func (d *DryRunClient) ConfigUpdate(ctx context.Context, id string, options client.ConfigUpdateOptions) (client.ConfigUpdateResult, error) {
return d.apiClient.ConfigUpdate(ctx, id, options)
}
func (d *DryRunClient) ContainerCommit(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
return d.apiClient.ContainerCommit(ctx, container, options)
}
func (d *DryRunClient) ContainerDiff(ctx context.Context, container string, options client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
return d.apiClient.ContainerDiff(ctx, container, options)
}
func (d *DryRunClient) ExecAttach(ctx context.Context, execID string, config client.ExecAttachOptions) (client.ExecAttachResult, error) {
return client.ExecAttachResult{}, errors.New("interactive exec is not supported in dry-run mode")
}
func (d *DryRunClient) ExecInspect(ctx context.Context, execID string, options client.ExecInspectOptions) (client.ExecInspectResult, error) {
return d.apiClient.ExecInspect(ctx, execID, options)
}
func (d *DryRunClient) ExecResize(ctx context.Context, execID string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
return d.apiClient.ExecResize(ctx, execID, options)
}
func (d *DryRunClient) ContainerExport(ctx context.Context, container string, options client.ContainerExportOptions) (client.ContainerExportResult, error) {
return d.apiClient.ContainerExport(ctx, container, options)
}
func (d *DryRunClient) ContainerLogs(ctx context.Context, container string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
return d.apiClient.ContainerLogs(ctx, container, options)
}
func (d *DryRunClient) ContainerResize(ctx context.Context, container string, options client.ContainerResizeOptions) (client.ContainerResizeResult, error) {
return d.apiClient.ContainerResize(ctx, container, options)
}
func (d *DryRunClient) ContainerStatPath(ctx context.Context, container string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
return d.apiClient.ContainerStatPath(ctx, container, options)
}
func (d *DryRunClient) ContainerStats(ctx context.Context, container string, options client.ContainerStatsOptions) (client.ContainerStatsResult, error) {
return d.apiClient.ContainerStats(ctx, container, options)
}
func (d *DryRunClient) ContainerTop(ctx context.Context, container string, options client.ContainerTopOptions) (client.ContainerTopResult, error) {
return d.apiClient.ContainerTop(ctx, container, options)
}
func (d *DryRunClient) ContainerUpdate(ctx context.Context, container string, options client.ContainerUpdateOptions) (client.ContainerUpdateResult, error) {
return d.apiClient.ContainerUpdate(ctx, container, options)
}
func (d *DryRunClient) ContainerWait(ctx context.Context, container string, options client.ContainerWaitOptions) client.ContainerWaitResult {
return d.apiClient.ContainerWait(ctx, container, options)
}
func (d *DryRunClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
return d.apiClient.ContainerPrune(ctx, options)
}
func (d *DryRunClient) DistributionInspect(ctx context.Context, imageName string, options client.DistributionInspectOptions) (client.DistributionInspectResult, error) {
return d.apiClient.DistributionInspect(ctx, imageName, options)
}
func (d *DryRunClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
return d.apiClient.BuildCachePrune(ctx, opts)
}
func (d *DryRunClient) BuildCancel(ctx context.Context, id string, opts client.BuildCancelOptions) (client.BuildCancelResult, error) {
return d.apiClient.BuildCancel(ctx, id, opts)
}
func (d *DryRunClient) ImageHistory(ctx context.Context, imageName string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) {
return d.apiClient.ImageHistory(ctx, imageName, options...)
}
func (d *DryRunClient) ImageImport(ctx context.Context, source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) {
return d.apiClient.ImageImport(ctx, source, ref, options)
}
func (d *DryRunClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
return d.apiClient.ImageList(ctx, options)
}
func (d *DryRunClient) ImageLoad(ctx context.Context, input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) {
return d.apiClient.ImageLoad(ctx, input, options...)
}
func (d *DryRunClient) ImageSearch(ctx context.Context, term string, options client.ImageSearchOptions) (client.ImageSearchResult, error) {
return d.apiClient.ImageSearch(ctx, term, options)
}
func (d *DryRunClient) ImageSave(ctx context.Context, images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) {
return d.apiClient.ImageSave(ctx, images, options...)
}
func (d *DryRunClient) ImageTag(ctx context.Context, options client.ImageTagOptions) (client.ImageTagResult, error) {
return d.apiClient.ImageTag(ctx, options)
}
func (d *DryRunClient) ImagePrune(ctx context.Context, options client.ImagePruneOptions) (client.ImagePruneResult, error) {
return d.apiClient.ImagePrune(ctx, options)
}
func (d *DryRunClient) NodeInspect(ctx context.Context, nodeID string, options client.NodeInspectOptions) (client.NodeInspectResult, error) {
return d.apiClient.NodeInspect(ctx, nodeID, options)
}
func (d *DryRunClient) NodeList(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) {
return d.apiClient.NodeList(ctx, options)
}
func (d *DryRunClient) NodeRemove(ctx context.Context, nodeID string, options client.NodeRemoveOptions) (client.NodeRemoveResult, error) {
return d.apiClient.NodeRemove(ctx, nodeID, options)
}
func (d *DryRunClient) NodeUpdate(ctx context.Context, nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) {
return d.apiClient.NodeUpdate(ctx, nodeID, options)
}
func (d *DryRunClient) NetworkInspect(ctx context.Context, networkName string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
return d.apiClient.NetworkInspect(ctx, networkName, options)
}
func (d *DryRunClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
return d.apiClient.NetworkList(ctx, options)
}
func (d *DryRunClient) NetworkPrune(ctx context.Context, options client.NetworkPruneOptions) (client.NetworkPruneResult, error) {
return d.apiClient.NetworkPrune(ctx, options)
}
func (d *DryRunClient) PluginList(ctx context.Context, options client.PluginListOptions) (client.PluginListResult, error) {
return d.apiClient.PluginList(ctx, options)
}
func (d *DryRunClient) PluginRemove(ctx context.Context, name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) {
return d.apiClient.PluginRemove(ctx, name, options)
}
func (d *DryRunClient) PluginEnable(ctx context.Context, name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) {
return d.apiClient.PluginEnable(ctx, name, options)
}
func (d *DryRunClient) PluginDisable(ctx context.Context, name string, options client.PluginDisableOptions) (client.PluginDisableResult, error) {
return d.apiClient.PluginDisable(ctx, name, options)
}
func (d *DryRunClient) PluginInstall(ctx context.Context, name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) {
return d.apiClient.PluginInstall(ctx, name, options)
}
func (d *DryRunClient) PluginUpgrade(ctx context.Context, name string, options client.PluginUpgradeOptions) (client.PluginUpgradeResult, error) {
return d.apiClient.PluginUpgrade(ctx, name, options)
}
func (d *DryRunClient) PluginPush(ctx context.Context, name string, options client.PluginPushOptions) (client.PluginPushResult, error) {
return d.apiClient.PluginPush(ctx, name, options)
}
func (d *DryRunClient) PluginSet(ctx context.Context, name string, options client.PluginSetOptions) (client.PluginSetResult, error) {
return d.apiClient.PluginSet(ctx, name, options)
}
func (d *DryRunClient) PluginInspect(ctx context.Context, name string, options client.PluginInspectOptions) (client.PluginInspectResult, error) {
return d.apiClient.PluginInspect(ctx, name, options)
}
func (d *DryRunClient) PluginCreate(ctx context.Context, createContext io.Reader, options client.PluginCreateOptions) (client.PluginCreateResult, error) {
return d.apiClient.PluginCreate(ctx, createContext, options)
}
func (d *DryRunClient) ServiceCreate(ctx context.Context, options client.ServiceCreateOptions) (client.ServiceCreateResult, error) {
return d.apiClient.ServiceCreate(ctx, options)
}
func (d *DryRunClient) ServiceInspect(ctx context.Context, serviceID string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) {
return d.apiClient.ServiceInspect(ctx, serviceID, options)
}
func (d *DryRunClient) ServiceList(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) {
return d.apiClient.ServiceList(ctx, options)
}
func (d *DryRunClient) ServiceRemove(ctx context.Context, serviceID string, options client.ServiceRemoveOptions) (client.ServiceRemoveResult, error) {
return d.apiClient.ServiceRemove(ctx, serviceID, options)
}
func (d *DryRunClient) ServiceUpdate(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) {
return d.apiClient.ServiceUpdate(ctx, serviceID, options)
}
func (d *DryRunClient) ServiceLogs(ctx context.Context, serviceID string, options client.ServiceLogsOptions) (client.ServiceLogsResult, error) {
return d.apiClient.ServiceLogs(ctx, serviceID, options)
}
func (d *DryRunClient) TaskLogs(ctx context.Context, taskID string, options client.TaskLogsOptions) (client.TaskLogsResult, error) {
return d.apiClient.TaskLogs(ctx, taskID, options)
}
func (d *DryRunClient) TaskInspect(ctx context.Context, taskID string, options client.TaskInspectOptions) (client.TaskInspectResult, error) {
return d.apiClient.TaskInspect(ctx, taskID, options)
}
func (d *DryRunClient) TaskList(ctx context.Context, options client.TaskListOptions) (client.TaskListResult, error) {
return d.apiClient.TaskList(ctx, options)
}
func (d *DryRunClient) SwarmInit(ctx context.Context, options client.SwarmInitOptions) (client.SwarmInitResult, error) {
return d.apiClient.SwarmInit(ctx, options)
}
func (d *DryRunClient) SwarmJoin(ctx context.Context, options client.SwarmJoinOptions) (client.SwarmJoinResult, error) {
return d.apiClient.SwarmJoin(ctx, options)
}
func (d *DryRunClient) SwarmGetUnlockKey(ctx context.Context) (client.SwarmGetUnlockKeyResult, error) {
return d.apiClient.SwarmGetUnlockKey(ctx)
}
func (d *DryRunClient) SwarmUnlock(ctx context.Context, options client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) {
return d.apiClient.SwarmUnlock(ctx, options)
}
func (d *DryRunClient) SwarmLeave(ctx context.Context, options client.SwarmLeaveOptions) (client.SwarmLeaveResult, error) {
return d.apiClient.SwarmLeave(ctx, options)
}
func (d *DryRunClient) SwarmInspect(ctx context.Context, options client.SwarmInspectOptions) (client.SwarmInspectResult, error) {
return d.apiClient.SwarmInspect(ctx, options)
}
func (d *DryRunClient) SwarmUpdate(ctx context.Context, options client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) {
return d.apiClient.SwarmUpdate(ctx, options)
}
func (d *DryRunClient) SecretList(ctx context.Context, options client.SecretListOptions) (client.SecretListResult, error) {
return d.apiClient.SecretList(ctx, options)
}
func (d *DryRunClient) SecretCreate(ctx context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) {
return d.apiClient.SecretCreate(ctx, options)
}
func (d *DryRunClient) SecretRemove(ctx context.Context, id string, options client.SecretRemoveOptions) (client.SecretRemoveResult, error) {
return d.apiClient.SecretRemove(ctx, id, options)
}
func (d *DryRunClient) SecretInspect(ctx context.Context, name string, options client.SecretInspectOptions) (client.SecretInspectResult, error) {
return d.apiClient.SecretInspect(ctx, name, options)
}
func (d *DryRunClient) SecretUpdate(ctx context.Context, id string, options client.SecretUpdateOptions) (client.SecretUpdateResult, error) {
return d.apiClient.SecretUpdate(ctx, id, options)
}
func (d *DryRunClient) Events(ctx context.Context, options client.EventsListOptions) client.EventsResult {
return d.apiClient.Events(ctx, options)
}
func (d *DryRunClient) Info(ctx context.Context, options client.InfoOptions) (client.SystemInfoResult, error) {
return d.apiClient.Info(ctx, options)
}
func (d *DryRunClient) RegistryLogin(ctx context.Context, options client.RegistryLoginOptions) (client.RegistryLoginResult, error) {
return d.apiClient.RegistryLogin(ctx, options)
}
func (d *DryRunClient) DiskUsage(ctx context.Context, options client.DiskUsageOptions) (client.DiskUsageResult, error) {
return d.apiClient.DiskUsage(ctx, options)
}
func (d *DryRunClient) Ping(ctx context.Context, options client.PingOptions) (client.PingResult, error) {
return d.apiClient.Ping(ctx, options)
}
func (d *DryRunClient) VolumeInspect(ctx context.Context, volumeID string, options client.VolumeInspectOptions) (client.VolumeInspectResult, error) {
return d.apiClient.VolumeInspect(ctx, volumeID, options)
}
func (d *DryRunClient) VolumeList(ctx context.Context, opts client.VolumeListOptions) (client.VolumeListResult, error) {
return d.apiClient.VolumeList(ctx, opts)
}
func (d *DryRunClient) VolumePrune(ctx context.Context, options client.VolumePruneOptions) (client.VolumePruneResult, error) {
return d.apiClient.VolumePrune(ctx, options)
}
func (d *DryRunClient) VolumeUpdate(ctx context.Context, volumeID string, options client.VolumeUpdateOptions) (client.VolumeUpdateResult, error) {
return d.apiClient.VolumeUpdate(ctx, volumeID, options)
}
func (d *DryRunClient) ClientVersion() string {
return d.apiClient.ClientVersion()
}
func (d *DryRunClient) DaemonHost() string {
return d.apiClient.DaemonHost()
}
func (d *DryRunClient) ServerVersion(ctx context.Context, options client.ServerVersionOptions) (client.ServerVersionResult, error) {
return d.apiClient.ServerVersion(ctx, options)
}
func (d *DryRunClient) DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) {
return d.apiClient.DialHijack(ctx, url, proto, meta)
}
func (d *DryRunClient) Dialer() func(context.Context) (net.Conn, error) {
return d.apiClient.Dialer()
}
func (d *DryRunClient) Close() error {
return d.apiClient.Close()
}
func (d *DryRunClient) CheckpointCreate(ctx context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
return d.apiClient.CheckpointCreate(ctx, container, options)
}
func (d *DryRunClient) CheckpointRemove(ctx context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
return d.apiClient.CheckpointRemove(ctx, container, options)
}
func (d *DryRunClient) CheckpointList(ctx context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
return d.apiClient.CheckpointList(ctx, container, options)
}
================================================
FILE: pkg/e2e/assert.go
================================================
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// RequireServiceState ensures that the container is in the expected state
// (running or exited).
func RequireServiceState(t testing.TB, cli *CLI, service string, state string) {
t.Helper()
psRes := cli.RunDockerComposeCmd(t, "ps", "--all", "--format=json", service)
var svc map[string]any
require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &svc),
"Invalid `compose ps` JSON: command output: %s",
psRes.Combined())
require.Equal(t, service, svc["Service"],
"Found ps output for unexpected service")
require.Equalf(t,
strings.ToLower(state),
strings.ToLower(svc["State"].(string)),
"Service %q (%s) not in expected state",
service, svc["Name"],
)
}
================================================
FILE: pkg/e2e/bridge_test.go
================================================
/*
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 (
"fmt"
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestConvertAndTransformList(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "bridge"
const bridgeImageVersion = "v0.0.3"
tmpDir := t.TempDir()
t.Run("kubernetes manifests", func(t *testing.T) {
kubedir := filepath.Join(tmpDir, "kubernetes")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
"--output", kubedir, "--transformation", fmt.Sprintf("docker/compose-bridge-kubernetes:%s", bridgeImageVersion))
assert.NilError(t, res.Error)
assert.Equal(t, res.ExitCode, 0)
res = c.RunCmd(t, "diff", "-r", kubedir, "./fixtures/bridge/expected-kubernetes")
assert.NilError(t, res.Error, res.Combined())
})
t.Run("helm charts", func(t *testing.T) {
helmDir := filepath.Join(tmpDir, "helm")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
"--output", helmDir, "--transformation", fmt.Sprintf("docker/compose-bridge-helm:%s", bridgeImageVersion))
assert.NilError(t, res.Error)
assert.Equal(t, res.ExitCode, 0)
res = c.RunCmd(t, "diff", "-r", helmDir, "./fixtures/bridge/expected-helm")
assert.NilError(t, res.Error, res.Combined())
})
t.Run("list transformers images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "bridge", "transformations",
"ls")
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-helm"), res.Combined())
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-kubernetes"), res.Combined())
})
}
================================================
FILE: pkg/e2e/build_test.go
================================================
/*
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 (
"fmt"
"net/http"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestLocalComposeBuild(t *testing.T) {
for _, env := range []string{"DOCKER_BUILDKIT=0", "DOCKER_BUILDKIT=1"} {
c := NewCLI(t, WithEnv(strings.Split(env, ",")...))
t.Run(env+" build named and unnamed images", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build")
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
c.RunDockerCmd(t, "image", "inspect", "custom-nginx")
})
t.Run(env+" build with build-arg", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--build-arg", "FOO=BAR")
res := c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
res.Assert(t, icmd.Expected{Out: `"FOO": "BAR"`})
})
t.Run(env+" build with build-arg set by env", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
icmd.RunCmd(c.NewDockerComposeCmd(t,
"--project-directory",
"fixtures/build-test",
"build",
"--build-arg",
"FOO"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "FOO=BAR")
}).Assert(t, icmd.Success)
res := c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
res.Assert(t, icmd.Expected{Out: `"FOO": "BAR"`})
})
t.Run(env+" build with multiple build-args ", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "multi-args-multiargs")
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/multi-args", "build")
icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
})
res := c.RunDockerCmd(t, "image", "inspect", "multi-args-multiargs")
res.Assert(t, icmd.Expected{Out: `"RESULT": "SUCCESS"`})
})
t.Run(env+" build as part of up", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
})
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
c.RunDockerCmd(t, "image", "inspect", "custom-nginx")
})
t.Run(env+" no rebuild when up again", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"))
})
t.Run(env+" rebuild when up --build", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d", "--build")
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
})
t.Run(env+" build --push ignored for unnamed images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--push", "nginx")
assert.Assert(t, !strings.Contains(res.Stdout(), "failed to push"), res.Stdout())
})
t.Run(env+" build --quiet", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--quiet")
res.Assert(t, icmd.Expected{Out: ""})
})
t.Run(env+" cleanup build project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
})
}
}
func TestBuildSSH(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Running on Windows. Skipping...")
}
c := NewParallelCLI(t)
t.Run("build failed with ssh default value", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--ssh", "")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "invalid empty ssh agent socket: make sure SSH_AUTH_SOCK is set",
})
})
t.Run("build succeed with ssh from Compose file", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "build-test-ssh")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/ssh", "build")
c.RunDockerCmd(t, "image", "inspect", "build-test-ssh")
})
t.Run("build succeed with ssh from CLI", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "build-test-ssh")
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/ssh/compose-without-ssh.yaml", "--project-directory",
"fixtures/build-test/ssh", "build", "--no-cache", "--ssh", "fake-ssh=./fixtures/build-test/ssh/fake_rsa")
c.RunDockerCmd(t, "image", "inspect", "build-test-ssh")
})
/*
FIXME disabled waiting for https://github.com/moby/buildkit/issues/5558
t.Run("build failed with wrong ssh key id from CLI", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "build-test-ssh")
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/build-test/ssh/compose-without-ssh.yaml",
"--project-directory", "fixtures/build-test/ssh", "build", "--no-cache", "--ssh",
"wrong-ssh=./fixtures/build-test/ssh/fake_rsa")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "unset ssh forward key fake-ssh",
})
})
*/
t.Run("build succeed as part of up with ssh from Compose file", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "build-test-ssh")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/ssh", "up", "-d", "--build")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/ssh", "down")
})
c.RunDockerCmd(t, "image", "inspect", "build-test-ssh")
})
}
func TestBuildSecrets(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on windows")
}
c := NewParallelCLI(t)
t.Run("build with secrets", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "build-test-secret")
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/secrets", "build")
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "SOME_SECRET=bar")
})
res.Assert(t, icmd.Success)
})
}
func TestBuildTags(t *testing.T) {
c := NewParallelCLI(t)
t.Run("build with tags", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "build-test-tags")
c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/tags", "build", "--no-cache")
res := c.RunDockerCmd(t, "image", "inspect", "build-test-tags")
expectedOutput := `"RepoTags": [
"docker/build-test-tags:1.0.0",
"build-test-tags:latest",
"other-image-name:v1.0.0"
],
`
res.Assert(t, icmd.Expected{Out: expectedOutput})
})
}
func TestBuildImageDependencies(t *testing.T) {
doTest := func(t *testing.T, cli *CLI, args ...string) {
resetState := func() {
cli.RunDockerComposeCmd(t, "down", "--rmi=all", "-t=0")
res := cli.RunDockerOrExitError(t, "image", "rm", "build-dependencies-service")
if res.Error != nil {
require.Contains(t, res.Stderr(), `No such image: build-dependencies-service`)
}
}
resetState()
t.Cleanup(resetState)
// the image should NOT exist now
res := cli.RunDockerOrExitError(t, "image", "inspect", "build-dependencies-service")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such image: build-dependencies-service",
})
res = cli.RunDockerComposeCmd(t, args...)
t.Log(res.Combined())
res = cli.RunDockerCmd(t,
"image", "inspect", "--format={{ index .RepoTags 0 }}",
"build-dependencies-service")
res.Assert(t, icmd.Expected{Out: "build-dependencies-service:latest"})
res = cli.RunDockerComposeCmd(t, "down", "-t0", "--rmi=all", "--remove-orphans")
t.Log(res.Combined())
res = cli.RunDockerOrExitError(t, "image", "inspect", "build-dependencies-service")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such image: build-dependencies-service",
})
}
t.Run("ClassicBuilder", func(t *testing.T) {
cli := NewCLI(t, WithEnv(
"DOCKER_BUILDKIT=0",
"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
))
doTest(t, cli, "build")
doTest(t, cli, "build", "--with-dependencies", "service")
})
t.Run("Bake by additional contexts", func(t *testing.T) {
cli := NewCLI(t, WithEnv(
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=1",
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
))
doTest(t, cli, "--verbose", "build")
doTest(t, cli, "--verbose", "build", "service")
doTest(t, cli, "--verbose", "up", "--build", "service")
})
}
func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Running on Windows. Skipping...")
}
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform")
})
t.Run("platform not supported by builder", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
"-f", "fixtures/build-test/platforms/compose-unsupported-platform.yml", "build")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "no match for platform",
})
})
t.Run("multi-arch build ok", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
})
t.Run("multi-arch multi service builds ok", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
"-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build")
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/amd64"})
res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/amd64"})
res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/amd64"})
})
t.Run("multi-arch up --build", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build", "--menu=false")
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "platforms-1 exited with code 0"})
})
t.Run("use DOCKER_DEFAULT_PLATFORM value when up --build", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build", "--menu=false")
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=linux/amd64")
})
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
assert.Assert(t, !strings.Contains(res.Stdout(), "I am building for linux/arm64"))
})
t.Run("use service platform value when no build platforms defined ", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
"-f", "fixtures/build-test/platforms/compose-service-platform-and-no-build-platforms.yaml", "build")
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "I am building for linux/386"})
})
}
func TestBuildPrivileged(t *testing.T) {
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-privileged", "--use", "--bootstrap", "--buildkitd-flags",
`'--allow-insecure-entitlement=security.insecure'`)
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-privileged")
})
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "build")
capEffRe := regexp.MustCompile("CapEff:\t([0-9a-f]+)")
matches := capEffRe.FindStringSubmatch(res.Stdout())
assert.Equal(t, 2, len(matches), "Did not match CapEff in output, matches: %v", matches)
capEff, err := strconv.ParseUint(matches[1], 16, 64)
assert.NilError(t, err, "Parsing CapEff: %s", matches[1])
// NOTE: can't use constant from x/sys/unix or tests won't compile on macOS/Windows
// #define CAP_SYS_ADMIN 21
// https://github.com/torvalds/linux/blob/v6.1/include/uapi/linux/capability.h#L278
const capSysAdmin = 0x15
if capEff&capSysAdmin != capSysAdmin {
t.Fatalf("CapEff %s is missing CAP_SYS_ADMIN", matches[1])
}
})
}
func TestBuildPlatformsStandardErrors(t *testing.T) {
c := NewParallelCLI(t)
t.Run("no platform support with Classic Builder", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
})
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the classic builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use BuildKit",
})
})
t.Run("builder does not support multi-arch", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "Multi-platform build is not supported for the docker driver.",
})
})
t.Run("service platform not defined in platforms build section", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
"-f", "fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml", "build")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: `service.build.platforms MUST include service.platform "linux/riscv64"`,
})
})
t.Run("DOCKER_DEFAULT_PLATFORM value not defined in platforms build section", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=windows/amd64")
})
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: `service "platforms" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: windows/amd64`,
})
})
t.Run("no privileged support with Classic Builder", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "build")
res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
})
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the classic builder doesn't support privileged mode, set DOCKER_BUILDKIT=1 to use BuildKit",
})
})
}
func TestBuildBuilder(t *testing.T) {
c := NewParallelCLI(t)
builderName := "build-with-builder"
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("use specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", builderName)
assert.NilError(t, res.Error, res.Stderr())
})
t.Run("error when using specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", "unknown-builder")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: fmt.Sprintf(`no builder %q found`, "unknown-builder"),
})
})
}
func TestBuildEntitlements(t *testing.T) {
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-insecure", "--use", "--bootstrap", "--buildkitd-flags",
`'--allow-insecure-entitlement=security.insecure'`)
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/entitlements", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-insecure")
})
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/entitlements", "build")
capEffRe := regexp.MustCompile("CapEff:\t([0-9a-f]+)")
matches := capEffRe.FindStringSubmatch(res.Stdout())
assert.Equal(t, 2, len(matches), "Did not match CapEff in output, matches: %v", matches)
capEff, err := strconv.ParseUint(matches[1], 16, 64)
assert.NilError(t, err, "Parsing CapEff: %s", matches[1])
// NOTE: can't use constant from x/sys/unix or tests won't compile on macOS/Windows
// #define CAP_SYS_ADMIN 21
// https://github.com/torvalds/linux/blob/v6.1/include/uapi/linux/capability.h#L278
const capSysAdmin = 0x15
if capEff&capSysAdmin != capSysAdmin {
t.Fatalf("CapEff %s is missing CAP_SYS_ADMIN", matches[1])
}
})
}
func TestBuildDependsOn(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2")
out := res.Combined()
assert.Check(t, strings.Contains(out, "test1 Built"))
}
func TestBuildSubset(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildDependentImage(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild")
out := res.Combined()
assert.Check(t, strings.Contains(out, "firstbuild Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild")
out = res.Combined()
assert.Check(t, strings.Contains(out, "secondbuild Built"))
}
func TestBuildSubDependencies(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main")
out = res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildLongOutputLine(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line")
out := res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line")
out = res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
}
func TestBuildDependentImageWithProfile(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test")
out := res.Combined()
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
}
func TestBuildTLS(t *testing.T) {
t.Helper()
c := NewParallelCLI(t)
const dindBuilder = "e2e-dind-builder"
tmp := t.TempDir()
t.Cleanup(func() {
c.RunDockerCmd(t, "rm", "-f", dindBuilder)
c.RunDockerCmd(t, "context", "rm", dindBuilder)
})
c.RunDockerCmd(t, "run", "--name", dindBuilder, "--privileged", "-p", "2376:2376", "-d", "docker:dind")
poll.WaitOn(t, func(_ poll.LogT) poll.Result {
res := c.RunDockerCmd(t, "logs", dindBuilder)
if strings.Contains(res.Combined(), "API listen on [::]:2376") {
return poll.Success()
}
return poll.Continue("waiting for Docker daemon to be running")
}, poll.WithTimeout(10*time.Second))
time.Sleep(1 * time.Second) // wait for dind setup
c.RunDockerCmd(t, "cp", dindBuilder+":/certs/client", tmp)
c.RunDockerCmd(t, "context", "create", dindBuilder, "--docker",
fmt.Sprintf("host=tcp://localhost:2376,ca=%s/client/ca.pem,cert=%s/client/cert.pem,key=%s/client/key.pem,skip-tls-verify=1", tmp, tmp, tmp))
cmd := c.NewDockerComposeCmd(t, "-f", "fixtures/build-test/minimal/compose.yaml", "build")
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT="+dindBuilder)
cmd.Stdout = os.Stdout
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{Err: "Built"})
}
func TestBuildEscaped(t *testing.T) {
c := NewParallelCLI(t)
res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "foo")
res.Assert(t, icmd.Expected{Out: "foo is ${bar}"})
res = c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "echo")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "arg")
res.Assert(t, icmd.Success)
}
================================================
FILE: pkg/e2e/cancel_test.go
================================================
//go:build !windows
/*
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 (
"context"
"fmt"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"github.com/docker/compose/v5/pkg/utils"
)
func TestComposeCancel(t *testing.T) {
c := NewParallelCLI(t)
t.Run("metrics on cancel Compose build", func(t *testing.T) {
const buildProjectPath = "fixtures/build-infinite/compose.yaml"
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
// require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal.
// sending kill signal
var stdout, stderr utils.SafeBuffer
cmd, err := StartWithNewGroupID(
ctx,
c.NewDockerComposeCmd(t, "-f", buildProjectPath, "build", "--progress", "plain"),
&stdout,
&stderr,
)
assert.NilError(t, err)
processDone := make(chan error, 1)
go func() {
defer close(processDone)
processDone <- cmd.Wait()
}()
c.WaitForCondition(t, func() (bool, string) {
out := stdout.String()
errors := stderr.String()
return strings.Contains(out,
"RUN sleep infinity"), fmt.Sprintf("'RUN sleep infinity' not found in : \n%s\nStderr: \n%s\n", out,
errors)
}, 30*time.Second, 1*time.Second)
// simulate Ctrl-C : send signal to processGroup, children will have same groupId by default
err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
assert.NilError(t, err)
select {
case <-ctx.Done():
t.Fatal("test context canceled")
case err := <-processDone:
// TODO(milas): Compose should really not return exit code 130 here,
// this is an old hack for the compose-cli wrapper
assert.Error(t, err, "exit status 130",
"STDOUT:\n%s\nSTDERR:\n%s\n", stdout.String(), stderr.String())
case <-time.After(10 * time.Second):
t.Fatal("timeout waiting for Compose exit")
}
})
}
func StartWithNewGroupID(ctx context.Context, command icmd.Cmd, stdout *utils.SafeBuffer, stderr *utils.SafeBuffer) (*exec.Cmd, error) {
cmd := exec.CommandContext(ctx, command.Command[0], command.Command[1:]...)
cmd.Env = command.Env
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if stdout != nil {
cmd.Stdout = stdout
}
if stderr != nil {
cmd.Stderr = stderr
}
err := cmd.Start()
return cmd, err
}
================================================
FILE: pkg/e2e/cascade_test.go
================================================
//go:build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestCascadeStop(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-cascade-stop"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
"up", "--abort-on-container-exit")
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
// no --exit-code-from, so this is not an error
assert.Equal(t, res.ExitCode, 0)
}
func TestCascadeFail(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-cascade-fail"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
"up", "--abort-on-container-failure")
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "fail-1 exited with code 111"), res.Combined())
// failing exit code should be propagated
assert.Equal(t, res.ExitCode, 111)
}
================================================
FILE: pkg/e2e/commit_test.go
================================================
/*
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 TestCommit(t *testing.T) {
const projectName = "e2e-commit-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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service")
c.RunDockerComposeCmd(
t,
"--project-name",
projectName,
"commit",
"-a",
"John Hannibal Smith ",
"-c",
"ENV DEBUG=true",
"-m",
"sample commit",
"service",
"service:latest",
)
}
func TestCommitWithReplicas(t *testing.T) {
const projectName = "e2e-commit-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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas")
c.RunDockerComposeCmd(
t,
"--project-name",
projectName,
"commit",
"-a",
"John Hannibal Smith ",
"-c",
"ENV DEBUG=true",
"-m",
"sample commit",
"--index=1",
"service-with-replicas",
"service-with-replicas:1",
)
c.RunDockerComposeCmd(
t,
"--project-name",
projectName,
"commit",
"-a",
"John Hannibal Smith ",
"-c",
"ENV DEBUG=true",
"-m",
"sample commit",
"--index=2",
"service-with-replicas",
"service-with-replicas:2",
)
}
================================================
FILE: pkg/e2e/compose_environment_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
func TestEnvPriority(t *testing.T) {
c := NewParallelCLI(t)
t.Run("up", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "env-compose-priority")
c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env.yaml",
"up", "-d", "--build")
})
// Full options activated
// 1. Command Line (docker compose run --env ) <-- Result expected (From OS Environment)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("compose file priority", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
cmd.Env = append(cmd.Env, "WHEREAMI=shell")
res := icmd.RunCmd(cmd)
assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell")
})
// Full options activated
// 1. Command Line (docker compose run --env ) <-- Result expected
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("compose file priority", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override",
"run", "--rm", "-e", "WHEREAMI=shell", "env-compose-priority")
res := icmd.RunCmd(cmd)
assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell")
})
// No Compose file, all other options
// 1. Command Line (docker compose run --env ) <-- Result expected (From OS Environment)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("shell priority", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
cmd.Env = append(cmd.Env, "WHEREAMI=shell")
res := icmd.RunCmd(cmd)
assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell")
})
// No Compose file, all other options with env variable from OS environment
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("shell priority file with default value", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override.with.default",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
cmd.Env = append(cmd.Env, "WHEREAMI=shell")
res := icmd.RunCmd(cmd)
assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell")
})
// No Compose file, all other options with env variable from OS environment
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment default value from file in --env-file)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("shell priority implicitly set", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override.with.default",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
res := icmd.RunCmd(cmd)
assert.Equal(t, strings.TrimSpace(res.Stdout()), "EnvFileDefaultValue")
})
// No Compose file, all other options with env variable from OS environment
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment default value from file in COMPOSE_ENV_FILES)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("shell priority from COMPOSE_ENV_FILES variable", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
cmd.Env = append(cmd.Env, "COMPOSE_ENV_FILES=./fixtures/environment/env-priority/.env.override.with.default")
res := icmd.RunCmd(cmd)
stdout := res.Stdout()
assert.Equal(t, strings.TrimSpace(stdout), "EnvFileDefaultValue")
})
// No Compose file and env variable pass to the run command
// 1. Command Line (docker compose run --env ) <-- Result expected
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("shell priority from run command", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override",
"run", "--rm", "-e", "WHEREAMI=shell-run", "env-compose-priority")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell-run")
})
// No Compose file & no env variable but override env file
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment patched by .env as a default --env-file value)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("override env file from compose", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env-file.yaml",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "Env File")
})
// No Compose file & no env variable but override by default env file
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment patched by --env-file value)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("override env file", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.override",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "override")
})
// No Compose file & no env variable but override env file
// 1. Command Line (docker compose run --env ) <-- Result expected (From environment patched by --env-file value)
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive
// 5. Variable is not defined
t.Run("env file", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "Env File")
})
// No Compose file & no env variable, using an empty override env file
// 1. Command Line (docker compose run --env )
// 2. Compose File (service::environment section)
// 3. Compose File (service::env_file section file)
// 4. Container Image ENV directive <-- Result expected
// 5. Variable is not defined
t.Run("use Dockerfile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml",
"--env-file", "./fixtures/environment/env-priority/.env.empty",
"run", "--rm", "-e", "WHEREAMI", "env-compose-priority")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "Dockerfile")
})
t.Run("down", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-name", "env-priority", "down")
})
}
func TestEnvInterpolation(t *testing.T) {
c := NewParallelCLI(t)
t.Run("shell priority from run command", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-interpolation/compose.yaml", "config")
cmd.Env = append(cmd.Env, "WHEREAMI=shell")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{Out: `IMAGE: default_env:shell`})
})
t.Run("shell priority from run command using default value fallback", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-interpolation-default-value/compose.yaml", "config").
Assert(t, icmd.Expected{Out: `IMAGE: default_env:EnvFileDefaultValue`})
})
}
func TestCommentsInEnvFile(t *testing.T) {
c := NewParallelCLI(t)
t.Run("comments in env files", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "env-file-comments")
c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-file-comments/compose.yaml", "up", "-d", "--build")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-file-comments/compose.yaml",
"run", "--rm", "-e", "COMMENT", "-e", "NO_COMMENT", "env-file-comments")
res.Assert(t, icmd.Expected{Out: `COMMENT=1234`})
res.Assert(t, icmd.Expected{Out: `NO_COMMENT=1234#5`})
c.RunDockerComposeCmd(t, "--project-name", "env-file-comments", "down", "--rmi", "all")
})
}
func TestUnsetEnv(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", "empty-variable", "down", "--rmi", "all")
})
t.Run("override env variable", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/empty-variable/compose.yaml", "build")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/empty-variable/compose.yaml",
"run", "-e", "EMPTY=hello", "--rm", "empty-variable")
res.Assert(t, icmd.Expected{Out: `=hello=`})
})
t.Run("unset env variable", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/empty-variable/compose.yaml",
"run", "--rm", "empty-variable")
res.Assert(t, icmd.Expected{Out: `==`})
})
}
================================================
FILE: pkg/e2e/compose_exec_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
func TestLocalComposeExec(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-exec"
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("up", "-d")...)
t.Run("exec true", func(t *testing.T) {
c.RunDockerComposeCmd(t, cmdArgs("exec", "simple", "/bin/true")...)
})
t.Run("exec false", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, cmdArgs("exec", "simple", "/bin/false")...)
res.Assert(t, icmd.Expected{ExitCode: 1})
})
t.Run("exec with env set", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, cmdArgs("exec", "-e", "FOO", "simple", "/usr/bin/env")...),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "FOO=BAR")
})
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
t.Run("exec without env set", 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())
})
}
func TestLocalComposeExecOneOff(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-exec-one-off"
defer c.cleanupWithDown(t, projectName)
cmdArgs := func(cmd string, args ...string) []string {
ret := []string{"--project-directory", "fixtures/simple-composefile", "--project-name", projectName, cmd}
ret = append(ret, args...)
return ret
}
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"})
})
cmdResult := c.RunDockerCmd(t, "ps", "-q", "--filter", "label=com.docker.compose.project=compose-e2e-exec-one-off").Stdout()
containerIDs := strings.Split(cmdResult, "\n")
_ = c.RunDockerOrExitError(t, append([]string{"stop"}, containerIDs...)...)
}
================================================
FILE: pkg/e2e/compose_run_build_once_test.go
================================================
/*
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 (
"crypto/rand"
"encoding/hex"
"fmt"
"regexp"
"strings"
"testing"
"gotest.tools/v3/assert"
)
// TestRunBuildOnce tests that services with pull_policy: build are only built once
// when using 'docker compose run', even when they are dependencies.
// This addresses a bug where dependencies were built twice: once in startDependencies
// and once in ensureImagesExists.
func TestRunBuildOnce(t *testing.T) {
c := NewParallelCLI(t)
t.Run("dependency with pull_policy build is built only once", func(t *testing.T) {
projectName := randomProjectName("build-once")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "--verbose", "run", "--build", "--rm", "curl")
output := res.Stdout()
nginxBuilds := countServiceBuilds(output, projectName, "nginx")
assert.Equal(t, nginxBuilds, 1, "nginx should build once, built %d times\nOutput:\n%s", nginxBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "curl service"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--remove-orphans")
})
t.Run("nested dependencies build only once each", func(t *testing.T) {
projectName := randomProjectName("build-nested")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "--verbose", "run", "--build", "--rm", "app")
output := res.Stdout()
dbBuilds := countServiceBuilds(output, projectName, "db")
apiBuilds := countServiceBuilds(output, projectName, "api")
appBuilds := countServiceBuilds(output, projectName, "app")
assert.Equal(t, dbBuilds, 1, "db should build once, built %d times\nOutput:\n%s", dbBuilds, output)
assert.Equal(t, apiBuilds, 1, "api should build once, built %d times\nOutput:\n%s", apiBuilds, output)
assert.Equal(t, appBuilds, 1, "app should build once, built %d times\nOutput:\n%s", appBuilds, output)
assert.Assert(t, strings.Contains(output, "App running"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
})
t.Run("service with no dependencies builds once", func(t *testing.T) {
projectName := randomProjectName("build-simple")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--rmi", "local", "--remove-orphans")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "run", "--build", "--rm", "simple")
output := res.Stdout()
simpleBuilds := countServiceBuilds(output, projectName, "simple")
assert.Equal(t, simpleBuilds, 1, "simple should build once, built %d times\nOutput:\n%s", simpleBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "Simple service"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--remove-orphans")
})
}
// countServiceBuilds counts how many times a service was built by matching
// the "naming to *{projectName}-{serviceName}* done" pattern in the output
func countServiceBuilds(output, projectName, serviceName string) int {
pattern := regexp.MustCompile(`naming to .*` + regexp.QuoteMeta(projectName) + `-` + regexp.QuoteMeta(serviceName) + `.* done`)
return len(pattern.FindAllString(output, -1))
}
// randomProjectName generates a unique project name for parallel test execution
// Format: prefix-<8 random hex chars> (e.g., "build-once-3f4a9b2c")
func randomProjectName(prefix string) string {
b := make([]byte, 4) // 4 bytes = 8 hex chars
rand.Read(b) //nolint:errcheck
return fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(b))
}
================================================
FILE: pkg/e2e/compose_run_test.go
================================================
/*
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 (
"os"
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestLocalComposeRun(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "run-test")
t.Run("compose run", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back")
lines := Lines(res.Stdout())
assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo",
"Hello one more time")
lines = Lines(res.Stdout())
assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout())
assert.Assert(t, strings.Contains(res.Combined(), "orphan"))
})
t.Run("check run container exited", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--all")
lines := Lines(res.Stdout())
var runContainerID string
var truncatedSlug string
for _, line := range lines {
fields := strings.Fields(line)
containerID := fields[len(fields)-1]
assert.Assert(t, !strings.HasPrefix(containerID, "run-test-front"))
if strings.HasPrefix(containerID, "run-test-back") {
// only the one-off container for back service
assert.Assert(t, strings.HasPrefix(containerID, "run-test-back-run-"), containerID)
truncatedSlug = strings.Replace(containerID, "run-test-back-run-", "", 1)
runContainerID = containerID
}
if strings.HasPrefix(containerID, "run-test-db-1") {
assert.Assert(t, strings.Contains(line, "Up"), line)
}
}
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.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})
})
t.Run("compose run --rm", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo",
"Hello again")
lines := Lines(res.Stdout())
assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
res = c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, strings.Contains(res.Stdout(), "run-test-back"), res.Stdout())
})
t.Run("down", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "down", "--remove-orphans")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
})
t.Run("compose run --volumes", func(t *testing.T) {
wd, err := os.Getwd()
assert.NilError(t, err)
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo",
"back", "/bin/sh", "-c", "ls /foo")
res.Assert(t, icmd.Expected{Out: "compose_run_test.go"})
res = c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, strings.Contains(res.Stdout(), "run-test-back"), res.Stdout())
})
t.Run("compose run --publish", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/ports.yaml", "run", "--publish", "8081:80", "-d", "back",
"/bin/sh", "-c", "sleep 1")
res := c.RunDockerCmd(t, "ps")
assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
assert.Assert(t, !strings.Contains(res.Stdout(), "8082->80/tcp"), res.Stdout())
})
t.Run("compose run --service-ports", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/ports.yaml", "run", "--service-ports", "-d", "back",
"/bin/sh", "-c", "sleep 1")
res := c.RunDockerCmd(t, "ps")
assert.Assert(t, strings.Contains(res.Stdout(), "8082->80/tcp"), res.Stdout())
})
t.Run("compose run orphan", func(t *testing.T) {
// Use different compose files to get an orphan container
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/orphan.yaml", "run", "simple")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
assert.Assert(t, strings.Contains(res.Combined(), "orphan"))
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True")
})
assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
})
t.Run("down", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "down")
icmd.RunCmd(cmd, func(c *icmd.Cmd) {
c.Env = append(c.Env, "COMPOSE_REMOVE_ORPHANS=True")
})
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
})
t.Run("run starts only container and dependencies", func(t *testing.T) {
// ensure that even if another service is up run does not start it: https://github.com/docker/compose/issues/9459
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "up", "service_b", "--menu=false")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "run", "service_a")
assert.Assert(t, strings.Contains(res.Combined(), "shared_dep"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "service_b"), res.Combined())
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans")
})
t.Run("run without dependencies", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "run", "--no-deps", "service_a")
assert.Assert(t, !strings.Contains(res.Combined(), "shared_dep"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "service_b"), res.Combined())
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans")
})
t.Run("run with not required dependency", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "run", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "bar"), res.Combined())
c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "down", "--remove-orphans")
})
t.Run("--quiet-pull", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--remove-orphans", "--rmi", "all")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "--quiet-pull", "backend")
assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Pulled"), res.Combined())
})
t.Run("COMPOSE_PROGRESS quiet", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--remove-orphans", "--rmi", "all")
res.Assert(t, icmd.Success)
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "backend")
res = icmd.RunCmd(cmd, func(c *icmd.Cmd) {
c.Env = append(c.Env, "COMPOSE_PROGRESS=quiet")
})
assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Pulled"), res.Combined())
})
t.Run("--pull", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "down", "--remove-orphans", "--rmi", "all")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "run", "--pull", "always", "backend")
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined())
})
t.Run("compose run --env-from-file", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env-from-file", "./fixtures/run-test/run.env",
"front", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
t.Run("compose run -rm with stop signal", func(t *testing.T) {
projectName := "run-test"
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "-f", "./fixtures/ps-test/compose.yaml", "run", "--rm", "-d", "nginx")
res.Assert(t, icmd.Success)
res = c.RunDockerCmd(t, "ps", "--quiet", "--filter", "name=run-test-nginx")
containerID := strings.TrimSpace(res.Stdout())
res = c.RunDockerCmd(t, "stop", containerID)
res.Assert(t, icmd.Success)
res = c.RunDockerCmd(t, "ps", "--all", "--filter", "name=run-test-nginx", "--format", "'{{.Names}}'")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test-nginx"), res.Stdout())
})
t.Run("compose run --env", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env", "FOO=BAR",
"front", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
t.Run("compose run --build", func(t *testing.T) {
c.cleanupWithDown(t, "run-test", "--rmi=local")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "build", "echo", "hello world")
res.Assert(t, icmd.Expected{Out: "hello world"})
})
t.Run("compose run with piped input detection", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that piped input is properly detected and TTY is automatically disabled
// This tests the logic added in run.go that checks dockerCli.In().IsTerminal()
cmd := c.NewCmd("sh", "-c", "echo 'piped-content' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm piped-test")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{Out: "piped-content"})
res.Assert(t, icmd.Success)
})
t.Run("compose run piped input should not allocate TTY", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that when stdin is piped, the container correctly detects no TTY
// This verifies that the automatic noTty=true setting works correctly
cmd := c.NewCmd("sh", "-c", "echo '' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm tty-test")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{Out: "No TTY detected"})
res.Assert(t, icmd.Success)
})
t.Run("compose run piped input with explicit --tty should fail", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that explicitly requesting TTY with piped input fails with proper error message
// This should trigger the "input device is not a TTY" error
cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --tty piped-test")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the input device is not a TTY",
})
})
t.Run("compose run piped input with --no-TTY=false should fail", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that explicitly disabling --no-TTY (i.e., requesting TTY) with piped input fails
// This should also trigger the "input device is not a TTY" error
cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --no-TTY=false piped-test")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the input device is not a TTY",
})
})
}
================================================
FILE: pkg/e2e/compose_test.go
================================================
/*
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 (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
testify "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestLocalComposeUp(t *testing.T) {
// this test shares a fixture with TestCompatibility and can't run at the same time
c := NewCLI(t)
const projectName = "compose-e2e-demo"
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("check accessing running app", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:90"
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
res = c.RunDockerCmd(t, "network", "ls")
res.Assert(t, icmd.Expected{Out: projectName + "_default"})
})
t.Run("top", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "top")
output := res.Stdout()
head := []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}
for _, h := range head {
assert.Assert(t, strings.Contains(output, h), output)
}
assert.Assert(t, strings.Contains(output, `java -Xmx8m -Xms8m -jar /app/words.jar`), output)
assert.Assert(t, strings.Contains(output, `/dispatcher`), output)
})
t.Run("check compose labels", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-web-1")
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.container-number": "1"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "compose-e2e-demo"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "False",`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.config-hash":`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project.config_files":`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project.working_dir":`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.service": "web"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.version":`})
res = c.RunDockerCmd(t, "network", "inspect", projectName+"_default")
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.network": "default"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": `})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.version": `})
})
t.Run("check user labels", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-web-1")
res.Assert(t, icmd.Expected{Out: `"my-label": "test"`})
})
t.Run("check healthcheck output", func(t *testing.T) {
c.WaitForCmdResult(t, c.NewDockerComposeCmd(t, "-p", projectName, "ps", "--format", "json"),
IsHealthy(projectName+"-web-1"),
5*time.Second, 1*time.Second)
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
assertServiceStatus(t, projectName, "web", "(healthy)", res.Stdout())
})
t.Run("images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "images")
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-db-1 gtardif/sentences-db latest`})
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-web-1 gtardif/sentences-web latest`})
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-words-1 gtardif/sentences-api latest`})
})
t.Run("down SERVICE", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "web")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), "compose-e2e-demo-web-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "compose-e2e-demo-db-1"), res.Combined())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("check containers after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
})
t.Run("check networks after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
})
}
func TestDownComposefileInParentFolder(t *testing.T) {
c := NewParallelCLI(t)
tmpFolder, err := os.MkdirTemp("fixtures/simple-composefile", "test-tmp")
assert.NilError(t, err)
defer os.Remove(tmpFolder) //nolint:errcheck
projectName := filepath.Base(tmpFolder)
res := c.RunDockerComposeCmd(t, "--project-directory", tmpFolder, "up", "-d")
res.Assert(t, icmd.Expected{Err: "Started", ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "down")
res.Assert(t, icmd.Expected{Err: "Removed", ExitCode: 0})
}
func TestAttachRestart(t *testing.T) {
t.Skip("Skipping test until we can fix it")
if _, ok := os.LookupEnv("CI"); ok {
t.Skip("Skipping test on CI... flaky")
}
c := NewParallelCLI(t)
cmd := c.NewDockerComposeCmd(t, "--ansi=never", "--project-directory", "./fixtures/attach-restart", "up")
res := icmd.StartCmd(cmd)
defer c.RunDockerComposeCmd(t, "-p", "attach-restart", "down")
c.WaitForCondition(t, func() (bool, string) {
debug := res.Combined()
return strings.Count(res.Stdout(),
"failing-1 exited with code 1") == 3, fmt.Sprintf("'failing-1 exited with code 1' not found 3 times in : \n%s\n",
debug)
}, 4*time.Minute, 2*time.Second)
assert.Equal(t, strings.Count(res.Stdout(), "failing-1 | world"), 3, res.Combined())
}
func TestInitContainer(t *testing.T) {
c := NewParallelCLI(t)
res := c.RunDockerComposeCmd(t, "--ansi=never", "--project-directory", "./fixtures/init-container", "up", "--menu=false")
defer c.RunDockerComposeCmd(t, "-p", "init-container", "down")
testify.Regexp(t, "foo-1 | hello(?m:.*)bar-1 | world", res.Stdout())
}
func TestRm(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-rm"
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d")
})
t.Run("rm --stop --force simple", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm",
"--stop", "--force", "simple")
res.Assert(t, icmd.Expected{Err: "Removed", ExitCode: 0})
})
t.Run("check containers after rm", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-simple"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), projectName+"-another"), res.Combined())
})
t.Run("up (again)", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d")
})
t.Run("rm ---stop --force ", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm",
"--stop", "--force")
res.Assert(t, icmd.Expected{ExitCode: 0})
})
t.Run("check containers after rm", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-simple"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-another"), res.Combined())
})
t.Run("down", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-p", projectName, "down")
})
}
func TestCompatibility(t *testing.T) {
// this test shares a fixture with TestLocalComposeUp and can't run at the same time
c := NewCLI(t)
const projectName = "compose-e2e-compatibility"
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--compatibility", "-f", "./fixtures/sentences/compose.yaml", "--project-name",
projectName, "up", "-d")
})
t.Run("check container names", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--format", "{{.Names}}")
res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_web_1"})
res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_words_1"})
res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_db_1"})
})
t.Run("down", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-p", projectName, "down")
})
}
func TestConfig(t *testing.T) {
const projectName = "compose-e2e-config"
c := NewParallelCLI(t)
wd, err := os.Getwd()
assert.NilError(t, err)
t.Run("up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "config")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s
services:
nginx:
build:
context: %s
dockerfile: Dockerfile
networks:
default: null
networks:
default:
name: compose-e2e-config_default
`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
})
}
func TestConfigInterpolate(t *testing.T) {
const projectName = "compose-e2e-config-interpolate"
c := NewParallelCLI(t)
wd, err := os.Getwd()
assert.NilError(t, err)
t.Run("config", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "config", "--no-interpolate")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s
networks:
default:
name: compose-e2e-config-interpolate_default
services:
nginx:
build:
context: %s
dockerfile: ${MYVAR}
networks:
default: null
`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
})
}
func TestStopWithDependenciesAttached(t *testing.T) {
const projectName = "compose-e2e-stop-with-deps"
c := NewParallelCLI(t, WithEnv("COMMAND=echo hello"))
cleanup := func() {
c.RunDockerComposeCmd(t, "-p", projectName, "down", "--remove-orphans", "--timeout=0")
}
cleanup()
t.Cleanup(cleanup)
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "-p", projectName, "up", "--attach-dependencies", "foo", "--menu=false")
res.Assert(t, icmd.Expected{Out: "exited with code 0"})
}
func TestRemoveOrphaned(t *testing.T) {
const projectName = "compose-e2e-remove-orphaned"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "-p", projectName, "down", "--remove-orphans", "--timeout=0")
}
cleanup()
t.Cleanup(cleanup)
// run stack
c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "up", "-d")
// down "web" service with orphaned removed
c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "down", "--remove-orphans", "web")
// check "words" service has not been considered orphaned
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "ps", "--format", "{{.Name}}")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf("%s-words-1", projectName)})
}
func TestComposeFileSetByDotEnv(t *testing.T) {
c := NewCLI(t)
defer c.cleanupWithDown(t, "dotenv")
cmd := c.NewDockerComposeCmd(t, "config")
cmd.Dir = filepath.Join(".", "fixtures", "dotenv")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 0,
Out: "image: test:latest",
})
res.Assert(t, icmd.Expected{
Out: "image: enabled:profile",
})
}
func TestComposeFileSetByProjectDirectory(t *testing.T) {
c := NewCLI(t)
defer c.cleanupWithDown(t, "dotenv")
dir := filepath.Join(".", "fixtures", "dotenv", "development")
cmd := c.NewDockerComposeCmd(t, "--project-directory", dir, "config")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 0,
Out: "image: backend:latest",
})
}
func TestComposeFileSetByEnvFile(t *testing.T) {
c := NewCLI(t)
defer c.cleanupWithDown(t, "dotenv")
dotEnv, err := os.CreateTemp(t.TempDir(), ".env")
assert.NilError(t, err)
err = os.WriteFile(dotEnv.Name(), []byte(`
COMPOSE_FILE=fixtures/dotenv/development/compose.yaml
IMAGE_NAME=test
IMAGE_TAG=latest
COMPOSE_PROFILES=test
`), 0o700)
assert.NilError(t, err)
cmd := c.NewDockerComposeCmd(t, "--env-file", dotEnv.Name(), "config")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
Out: "image: test:latest",
})
res.Assert(t, icmd.Expected{
Out: "image: enabled:profile",
})
}
func TestNestedDotEnv(t *testing.T) {
c := NewCLI(t)
defer c.cleanupWithDown(t, "nested")
cmd := c.NewDockerComposeCmd(t, "run", "echo")
cmd.Dir = filepath.Join(".", "fixtures", "nested")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 0,
Out: "root win=root",
})
cmd = c.NewDockerComposeCmd(t, "run", "echo")
cmd.Dir = filepath.Join(".", "fixtures", "nested", "sub")
defer c.cleanupWithDown(t, "nested")
res = icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{
ExitCode: 0,
Out: "root sub win=sub",
})
}
func TestUnnecessaryResources(t *testing.T) {
const projectName = "compose-e2e-unnecessary-resources"
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, projectName)
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/external/compose.yaml", "-p", projectName, "up", "-d")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "network foo_bar declared as external, but could not be found",
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/external/compose.yaml", "-p", projectName, "up", "-d", "test")
// Should not fail as missing external network is not used
}
================================================
FILE: pkg/e2e/compose_up_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestUpWait(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-deps-wait"
timeout := time.After(30 * time.Second)
done := make(chan bool)
go func() {
//nolint:nolintlint,testifylint // helper asserts inside goroutine; acceptable in this e2e test
res := c.RunDockerComposeCmd(t, "-f", "fixtures/dependencies/deps-completed-successfully.yaml", "--project-name", projectName, "up", "--wait", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-deps-wait-oneshot-1"), res.Combined())
done <- true
}()
select {
case <-timeout:
t.Fatal("test did not finish in time")
case <-done:
break
}
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
}
func TestUpExitCodeFrom(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--menu=false", "--exit-code-from=failure", "failure")
res.Assert(t, icmd.Expected{ExitCode: 42})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestUpExitCodeFromContainerKilled(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from-kill"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--menu=false", "--exit-code-from=test")
res.Assert(t, icmd.Expected{ExitCode: 143})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestPortRange(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-port-range"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans", "--timeout=0")
}
reset()
t.Cleanup(reset)
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/port-range/compose.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Success)
}
func TestStdoutStderr(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-stdout-stderr"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/stdout-stderr/compose.yaml", "--project-name", projectName, "up", "--menu=false")
res.Assert(t, icmd.Expected{Out: "log to stdout", Err: "log to stderr"})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestLoggingDriver(t *testing.T) {
c := NewCLI(t)
const projectName = "e2e-logging-driver"
defer c.cleanupWithDown(t, projectName)
host := "HOST=127.0.0.1"
res := c.RunDockerCmd(t, "info", "-f", "{{.OperatingSystem}}")
os := res.Stdout()
if strings.TrimSpace(os) == "Docker Desktop" {
host = "HOST=host.docker.internal"
}
cmd := c.NewDockerComposeCmd(t, "-f", "fixtures/logging-driver/compose.yaml", "--project-name", projectName, "up", "-d")
cmd.Env = append(cmd.Env, host, "BAR=foo")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd = c.NewDockerComposeCmd(t, "-f", "fixtures/logging-driver/compose.yaml", "--project-name", projectName, "up", "-d")
cmd.Env = append(cmd.Env, host, "BAR=zot")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
}
================================================
FILE: pkg/e2e/config_test.go
================================================
/*
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 (
"testing"
"gotest.tools/v3/icmd"
)
func TestLocalComposeConfig(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-config"
t.Run("yaml", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config")
res.Assert(t, icmd.Expected{Out: `
ports:
- mode: ingress
target: 80
published: "8080"
protocol: tcp`})
})
t.Run("json", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--format", "json")
res.Assert(t, icmd.Expected{Out: `"published": "8080"`})
})
t.Run("--no-interpolate", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--no-interpolate")
res.Assert(t, icmd.Expected{Out: `- ${PORT:-8080}:80`})
})
t.Run("--variables --format json", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--variables", "--format", "json")
res.Assert(t, icmd.Expected{Out: `{
"PORT": {
"Name": "PORT",
"DefaultValue": "8080",
"PresenceValue": "",
"Required": false
}
}`})
})
t.Run("--variables --format yaml", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--variables", "--format", "yaml")
res.Assert(t, icmd.Expected{Out: `PORT:
name: PORT
defaultvalue: "8080"
presencevalue: ""
required: false`})
})
}
================================================
FILE: pkg/e2e/configs_test.go
================================================
/*
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 (
"testing"
"gotest.tools/v3/icmd"
)
func TestConfigFromEnv(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "configs")
t.Run("config from file", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_file"))
res.Assert(t, icmd.Expected{Out: "This is my config file"})
})
t.Run("config from env", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_env"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "CONFIG=config")
})
res.Assert(t, icmd.Expected{Out: "config"})
})
t.Run("config inlined", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "inlined"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "CONFIG=config")
})
res.Assert(t, icmd.Expected{Out: "This is my config"})
})
t.Run("custom target", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "target"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "CONFIG=config")
})
res.Assert(t, icmd.Expected{Out: "This is my config"})
})
}
================================================
FILE: pkg/e2e/container_name_test.go
================================================
//go:build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"testing"
"gotest.tools/v3/icmd"
)
func TestUpContainerNameConflict(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-container_name_conflict"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container name "test" is already in use`})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunDockerComposeCmd(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up", "test")
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunDockerComposeCmd(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up", "another_test")
}
================================================
FILE: pkg/e2e/cp_test.go
================================================
/*
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 (
"os"
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestCopy(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "copy_e2e"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "down")
os.Remove("./fixtures/cp-test/from-default.txt") //nolint:errcheck
os.Remove("./fixtures/cp-test/from-indexed.txt") //nolint:errcheck
os.RemoveAll("./fixtures/cp-test/cp-folder2") //nolint:errcheck
})
t.Run("start service", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "--project-name", projectName, "up",
"--scale", "nginx=5", "-d")
})
t.Run("make sure service is running", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
assertServiceStatus(t, projectName, "nginx", "Up", res.Stdout())
})
t.Run("copy to container copies the file to the all containers by default", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp",
"./fixtures/cp-test/cp-me.txt", "nginx:/tmp/default.txt")
res.Assert(t, icmd.Expected{ExitCode: 0})
output := c.RunDockerCmd(t, "exec", projectName+"-nginx-1", "cat", "/tmp/default.txt").Stdout()
assert.Assert(t, strings.Contains(output, `hello world`), output)
output = c.RunDockerCmd(t, "exec", projectName+"-nginx-2", "cat", "/tmp/default.txt").Stdout()
assert.Assert(t, strings.Contains(output, `hello world`), output)
output = c.RunDockerCmd(t, "exec", projectName+"-nginx-3", "cat", "/tmp/default.txt").Stdout()
assert.Assert(t, strings.Contains(output, `hello world`), output)
})
t.Run("copy to container with a given index copies the file to the given container", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3",
"./fixtures/cp-test/cp-me.txt", "nginx:/tmp/indexed.txt")
res.Assert(t, icmd.Expected{ExitCode: 0})
output := c.RunDockerCmd(t, "exec", projectName+"-nginx-3", "cat", "/tmp/indexed.txt").Stdout()
assert.Assert(t, strings.Contains(output, `hello world`), output)
res = c.RunDockerOrExitError(t, "exec", projectName+"-nginx-2", "cat", "/tmp/indexed.txt")
res.Assert(t, icmd.Expected{ExitCode: 1})
})
t.Run("copy from a container copies the file to the host from the first container by default", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp",
"nginx:/tmp/default.txt", "./fixtures/cp-test/from-default.txt")
res.Assert(t, icmd.Expected{ExitCode: 0})
data, err := os.ReadFile("./fixtures/cp-test/from-default.txt")
assert.NilError(t, err)
assert.Equal(t, `hello world`, string(data))
})
t.Run("copy from a container with a given index copies the file to host", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp", "--index=3",
"nginx:/tmp/indexed.txt", "./fixtures/cp-test/from-indexed.txt")
res.Assert(t, icmd.Expected{ExitCode: 0})
data, err := os.ReadFile("./fixtures/cp-test/from-indexed.txt")
assert.NilError(t, err)
assert.Equal(t, `hello world`, string(data))
})
t.Run("copy to and from a container also work with folder", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp",
"./fixtures/cp-test/cp-folder", "nginx:/tmp")
res.Assert(t, icmd.Expected{ExitCode: 0})
output := c.RunDockerCmd(t, "exec", projectName+"-nginx-1", "cat", "/tmp/cp-folder/cp-me.txt").Stdout()
assert.Assert(t, strings.Contains(output, `hello world from folder`), output)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/cp-test/compose.yaml", "-p", projectName, "cp",
"nginx:/tmp/cp-folder", "./fixtures/cp-test/cp-folder2")
res.Assert(t, icmd.Expected{ExitCode: 0})
data, err := os.ReadFile("./fixtures/cp-test/cp-folder2/cp-me.txt")
assert.NilError(t, err)
assert.Equal(t, `hello world from folder`, string(data))
})
}
================================================
FILE: pkg/e2e/e2e_config_plugin.go
================================================
//go:build !standalone
/*
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
const composeStandaloneMode = false
================================================
FILE: pkg/e2e/e2e_config_standalone.go
================================================
//go:build standalone
/*
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
const composeStandaloneMode = true
================================================
FILE: pkg/e2e/env_file_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
func TestRawEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "dotenv")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dotenv/raw.yaml", "run", "test")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "'{\"key\": \"value\"}'")
}
func TestUnusedMissingEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "unused_dotenv")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "up", "-d", "serviceA")
// Runtime operations should work even with missing env file
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "ps")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "logs")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "down")
}
func TestRunEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "run_dotenv")
res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/env_file", "run", "serviceC", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
}
================================================
FILE: pkg/e2e/exec_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
func TestExec(t *testing.T) {
const projectName = "e2e-exec"
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/exec/compose.yaml", "--project-name", projectName, "run", "-d", "test", "cat")
res := c.RunDockerComposeCmdNoCheck(t, "--project-name", projectName, "exec", "--index=1", "test", "ps")
res.Assert(t, icmd.Expected{Err: "service \"test\" is not running container #1", ExitCode: 1})
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "ps")
res.Assert(t, icmd.Expected{Out: "cat"}) // one-off container was selected
c.RunDockerComposeCmd(t, "-f", "./fixtures/exec/compose.yaml", "--project-name", projectName, "up", "-d")
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "ps")
res.Assert(t, icmd.Expected{Out: "tail"}) // service container was selected
}
================================================
FILE: pkg/e2e/export_test.go
================================================
/*
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")
}
================================================
FILE: pkg/e2e/expose_test.go
================================================
//go:build !windows
/*
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 (
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
// see https://github.com/docker/compose/issues/13378
func TestExposeRange(t *testing.T) {
c := NewParallelCLI(t)
f := filepath.Join(t.TempDir(), "compose.yaml")
err := os.WriteFile(f, []byte(`
name: test-expose-range
services:
test:
image: alpine
expose:
- "9091-9092"
`), 0o644)
assert.NilError(t, err)
t.Cleanup(func() {
c.cleanupWithDown(t, "test-expose-range")
})
c.RunDockerComposeCmd(t, "-f", f, "up")
}
================================================
FILE: pkg/e2e/fixtures/attach-restart/compose.yaml
================================================
services:
failing:
image: alpine
command: sh -c "sleep 0.1 && echo world && /bin/false"
deploy:
restart_policy:
condition: "on-failure"
max_attempts: 2
================================================
FILE: pkg/e2e/fixtures/bridge/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
ENV ENV_FROM_DOCKERFILE=1
EXPOSE 8081
CMD ["echo", "Hello from Dockerfile"]
================================================
FILE: pkg/e2e/fixtures/bridge/compose.yaml
================================================
services:
serviceA:
image: alpine
build: .
ports:
- 80:8080
networks:
- private-network
configs:
- source: my-config
target: /etc/my-config1.txt
serviceB:
image: alpine
build: .
ports:
- 8081:8082
secrets:
- my-secrets
networks:
- private-network
- public-network
configs:
my-config:
file: my-config.txt
secrets:
my-secrets:
file: not-so-secret.txt
networks:
private-network:
internal: true
public-network: {}
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/Chart.yaml
================================================
#! Chart.yaml
apiVersion: v2
name: bridge
version: 0.0.1
# kubeVersion: >= 1.29.1
description: A generated Helm Chart for bridge generated via compose-bridge.
type: application
keywords:
- bridge
appVersion: 'v0.0.1'
sources:
annotations:
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/0-bridge-namespace.yaml
================================================
#! 0-bridge-namespace.yaml
# Generated code, do not edit
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/bridge-configs.yaml
================================================
#! bridge-configs.yaml
# Generated code, do not edit
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.projectName }}
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
data:
my-config: |
My config file
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/my-secrets-secret.yaml
================================================
#! my-secrets-secret.yaml
# Generated code, do not edit
apiVersion: v1
kind: Secret
metadata:
name: my-secrets
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.secret: my-secrets
data:
my-secrets: bm90LXNlY3JldA==
type: Opaque
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/private-network-network-policy.yaml
================================================
#! private-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: private-network-network-policy
namespace: {{ .Values.namespace }}
spec:
podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/public-network-network-policy.yaml
================================================
#! public-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: public-network-network-policy
namespace: {{ .Values.namespace }}
spec:
podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-deployment.yaml
================================================
#! serviceA-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicea
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
replicas: {{ .Values.deployment.defaultReplicas }}
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
strategy:
type: {{ .Values.deployment.strategy }}
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
com.docker.compose.network.private-network: "true"
spec:
containers:
- name: servicea
image: {{ .Values.serviceA.image }}
imagePullPolicy: {{ .Values.serviceA.imagePullPolicy }}
resources:
limits:
cpu: {{ .Values.resources.defaultCpuLimit }}
memory: {{ .Values.resources.defaultMemoryLimit }}
ports:
- name: servicea-8080
containerPort: 8080
volumeMounts:
- name: etc-my-config1-txt
mountPath: /etc/my-config1.txt
subPath: my-config
readOnly: true
volumes:
- name: etc-my-config1-txt
configMap:
name: {{ .Values.projectName }}
items:
- key: my-config
path: my-config
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-expose.yaml
================================================
#! serviceA-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-8080
port: 8080
targetPort: servicea-8080
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-service.yaml
================================================
# check if there is at least one published port
#! serviceA-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea-published
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
type: {{ .Values.service.type }}
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-80
port: 80
protocol: TCP
targetPort: servicea-8080
# check if there is at least one published port
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-deployment.yaml
================================================
#! serviceB-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: serviceb
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
replicas: {{ .Values.deployment.defaultReplicas }}
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
strategy:
type: {{ .Values.deployment.strategy }}
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
com.docker.compose.network.private-network: "true"
com.docker.compose.network.public-network: "true"
spec:
containers:
- name: serviceb
image: {{ .Values.serviceB.image }}
imagePullPolicy: {{ .Values.serviceB.imagePullPolicy }}
resources:
limits:
cpu: {{ .Values.resources.defaultCpuLimit }}
memory: {{ .Values.resources.defaultMemoryLimit }}
ports:
- name: serviceb-8082
containerPort: 8082
volumeMounts:
- name: run-secrets-my-secrets
mountPath: /run/secrets/my-secrets
subPath: my-secrets
readOnly: true
volumes:
- name: run-secrets-my-secrets
secret:
secretName: my-secrets
items:
- key: my-secrets
path: my-secrets
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-expose.yaml
================================================
#! serviceB-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8082
port: 8082
targetPort: serviceb-8082
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-service.yaml
================================================
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
type: {{ .Values.service.type }}
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8081
port: 8081
protocol: TCP
targetPort: serviceb-8082
================================================
FILE: pkg/e2e/fixtures/bridge/expected-helm/values.yaml
================================================
#! values.yaml
# Project Name
projectName: bridge
# Namespace
namespace: bridge
# Default deployment settings
deployment:
strategy: Recreate
defaultReplicas: 1
# Default resource limits
resources:
defaultCpuLimit: "100m"
defaultMemoryLimit: "512Mi"
# Service settings
service:
type: LoadBalancer
# Storage settings
storage:
defaultStorageClass: "hostpath"
defaultSize: "100Mi"
defaultAccessMode: "ReadWriteOnce"
# Services variables
serviceA:
image: alpine
imagePullPolicy: IfNotPresent
serviceB:
image: alpine
imagePullPolicy: IfNotPresent
# You can apply the same logic to loop on networks, volumes, secrets and configs...
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/0-bridge-namespace.yaml
================================================
#! 0-bridge-namespace.yaml
# Generated code, do not edit
apiVersion: v1
kind: Namespace
metadata:
name: bridge
labels:
com.docker.compose.project: bridge
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/bridge-configs.yaml
================================================
#! bridge-configs.yaml
# Generated code, do not edit
apiVersion: v1
kind: ConfigMap
metadata:
name: bridge
namespace: bridge
labels:
com.docker.compose.project: bridge
data:
my-config: |
My config file
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/kustomization.yaml
================================================
#! kustomization.yaml
# Generated code, do not edit
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 0-bridge-namespace.yaml
- bridge-configs.yaml
- my-secrets-secret.yaml
- private-network-network-policy.yaml
- public-network-network-policy.yaml
- serviceA-deployment.yaml
- serviceA-expose.yaml
- serviceA-service.yaml
- serviceB-deployment.yaml
- serviceB-expose.yaml
- serviceB-service.yaml
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/my-secrets-secret.yaml
================================================
#! my-secrets-secret.yaml
# Generated code, do not edit
apiVersion: v1
kind: Secret
metadata:
name: my-secrets
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.secret: my-secrets
data:
my-secrets: bm90LXNlY3JldA==
type: Opaque
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/private-network-network-policy.yaml
================================================
#! private-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: private-network-network-policy
namespace: bridge
spec:
podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/public-network-network-policy.yaml
================================================
#! public-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: public-network-network-policy
namespace: bridge
spec:
podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceA-deployment.yaml
================================================
#! serviceA-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicea
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
spec:
replicas: 1
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
strategy:
type: Recreate
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
com.docker.compose.network.private-network: "true"
spec:
containers:
- name: servicea
image: alpine
imagePullPolicy: IfNotPresent
ports:
- name: servicea-8080
containerPort: 8080
volumeMounts:
- name: etc-my-config1-txt
mountPath: /etc/my-config1.txt
subPath: my-config
readOnly: true
volumes:
- name: etc-my-config1-txt
configMap:
name: bridge
items:
- key: my-config
path: my-config
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceA-expose.yaml
================================================
#! serviceA-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-8080
port: 8080
targetPort: servicea-8080
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceA-service.yaml
================================================
# check if there is at least one published port
#! serviceA-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea-published
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-80
port: 80
protocol: TCP
targetPort: servicea-8080
# check if there is at least one published port
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceB-deployment.yaml
================================================
#! serviceB-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: serviceb
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
spec:
replicas: 1
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
strategy:
type: Recreate
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
com.docker.compose.network.private-network: "true"
com.docker.compose.network.public-network: "true"
spec:
containers:
- name: serviceb
image: alpine
imagePullPolicy: IfNotPresent
ports:
- name: serviceb-8082
containerPort: 8082
volumeMounts:
- name: run-secrets-my-secrets
mountPath: /run/secrets/my-secrets
subPath: my-secrets
readOnly: true
volumes:
- name: run-secrets-my-secrets
secret:
secretName: my-secrets
items:
- key: my-secrets
path: my-secrets
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceB-expose.yaml
================================================
#! serviceB-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8082
port: 8082
targetPort: serviceb-8082
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/base/serviceB-service.yaml
================================================
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8081
port: 8081
protocol: TCP
targetPort: serviceb-8082
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/overlays/desktop/kustomization.yaml
================================================
#! kustomization.yaml
# Generated code, do not edit
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: serviceA-service.yaml
- path: serviceB-service.yaml
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/overlays/desktop/serviceA-service.yaml
================================================
# check if there is at least one published port
#! serviceA-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea-published
namespace: bridge
spec:
type: LoadBalancer
# check if there is at least one published port
================================================
FILE: pkg/e2e/fixtures/bridge/expected-kubernetes/overlays/desktop/serviceB-service.yaml
================================================
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: bridge
spec:
type: LoadBalancer
================================================
FILE: pkg/e2e/fixtures/bridge/my-config.txt
================================================
My config file
================================================
FILE: pkg/e2e/fixtures/bridge/not-so-secret.txt
================================================
not-secret
================================================
FILE: pkg/e2e/fixtures/build-dependencies/base.dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
COPY hello.txt /hello.txt
CMD [ "/bin/true" ]
================================================
FILE: pkg/e2e/fixtures/build-dependencies/classic.yaml
================================================
services:
base:
image: base
init: true
build:
context: .
dockerfile: base.dockerfile
service:
init: true
depends_on:
- base
build:
context: .
dockerfile: service.dockerfile
================================================
FILE: pkg/e2e/fixtures/build-dependencies/compose-depends_on.yaml
================================================
services:
test1:
pull_policy: build
build:
dockerfile_inline: FROM alpine
command:
- echo
- "test 1 success"
test2:
image: alpine
depends_on:
- test1
command:
- echo
- "test 2 success"
================================================
FILE: pkg/e2e/fixtures/build-dependencies/compose.yaml
================================================
services:
base:
init: true
build:
context: .
dockerfile: base.dockerfile
service:
init: true
build:
context: .
additional_contexts:
base: "service:base"
dockerfile: service.dockerfile
================================================
FILE: pkg/e2e/fixtures/build-dependencies/hello.txt
================================================
this file was copied from base -> service
================================================
FILE: pkg/e2e/fixtures/build-dependencies/service.dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM base
CMD [ "cat", "/hello.txt" ]
================================================
FILE: pkg/e2e/fixtures/build-infinite/compose.yaml
================================================
services:
service1:
build: service1
================================================
FILE: pkg/e2e/fixtures/build-infinite/service1/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM busybox
RUN sleep infinity
================================================
FILE: pkg/e2e/fixtures/build-test/compose.yaml
================================================
services:
nginx:
build: nginx-build
ports:
- 8070:80
nginx2:
build: nginx-build2
image: custom-nginx
================================================
FILE: pkg/e2e/fixtures/build-test/dependencies/compose.yaml
================================================
services:
firstbuild:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
entrypoint: ["echo", "Hello from firstbuild"]
depends_on:
- dep1
secondbuild:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
entrypoint: ["echo", "Hello from secondbuild"]
depends_on:
- dep1
dep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep1"]
================================================
FILE: pkg/e2e/fixtures/build-test/entitlements/Dockerfile
================================================
# syntax = docker/dockerfile:experimental
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
RUN --security=insecure cat /proc/self/status | grep CapEff
================================================
FILE: pkg/e2e/fixtures/build-test/entitlements/compose.yaml
================================================
services:
privileged-service:
build:
context: .
entitlements:
- security.insecure
================================================
FILE: pkg/e2e/fixtures/build-test/escaped/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
ARG foo
RUN echo foo is $foo
================================================
FILE: pkg/e2e/fixtures/build-test/escaped/compose.yaml
================================================
services:
foo:
build:
context: .
args:
foo: $${bar}
echo:
build:
dockerfile_inline: |
FROM bash
RUN <<'EOF'
echo $(seq 10)
EOF
arg:
build:
args:
BOOL: "true"
dockerfile_inline: |
FROM alpine:latest
ARG BOOL
RUN /bin/$${BOOL}
================================================
FILE: pkg/e2e/fixtures/build-test/long-output-line/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
# We generate warnings *on purpose* to bloat the JSON output of bake
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
================================================
FILE: pkg/e2e/fixtures/build-test/long-output-line/compose.yaml
================================================
services:
long-line:
build:
context: .
dockerfile: Dockerfile
================================================
FILE: pkg/e2e/fixtures/build-test/minimal/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM scratch
COPY . .
================================================
FILE: pkg/e2e/fixtures/build-test/minimal/compose.yaml
================================================
services:
test:
build: .
================================================
FILE: pkg/e2e/fixtures/build-test/multi-args/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
ARG IMAGE=666
ARG TAG=666
FROM ${IMAGE}:${TAG}
RUN echo "SUCCESS"
================================================
FILE: pkg/e2e/fixtures/build-test/multi-args/compose.yaml
================================================
services:
multiargs:
build:
context: .
args:
IMAGE: alpine
TAG: latest
labels:
- RESULT=SUCCESS
================================================
FILE: pkg/e2e/fixtures/build-test/nginx-build/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
ARG FOO
LABEL FOO=$FOO
COPY static /usr/share/nginx/html
================================================
FILE: pkg/e2e/fixtures/build-test/nginx-build/static/index.html
================================================
Static file 2
Hello from Nginx container
================================================
FILE: pkg/e2e/fixtures/build-test/nginx-build2/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
COPY static2 /usr/share/nginx/html
================================================
FILE: pkg/e2e/fixtures/build-test/nginx-build2/static2/index.html
================================================
Static file 2
Hello from Nginx container
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml
================================================
services:
serviceA:
image: build-test-platform-a:test
build:
context: ./contextServiceA
platforms:
- linux/amd64
- linux/arm64
serviceB:
image: build-test-platform-b:test
build:
context: ./contextServiceB
platforms:
- linux/amd64
- linux/arm64
serviceC:
image: build-test-platform-c:test
build:
context: ./contextServiceC
platforms:
- linux/amd64
- linux/arm64
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/compose-service-platform-and-no-build-platforms.yaml
================================================
services:
platforms:
image: build-test-platform:test
platform: linux/386
build:
context: .
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml
================================================
services:
platforms:
image: build-test-platform:test
platform: linux/riscv64
build:
context: .
platforms:
- linux/amd64
- linux/arm64
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml
================================================
services:
platforms:
image: build-test-platform:test
build:
context: .
platforms:
- unsupported/unsupported
- linux/amd64
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/compose.yaml
================================================
services:
platforms:
image: build-test-platform:test
build:
context: .
platforms:
- linux/amd64
- linux/arm64
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I'm Service A and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I'm Service B and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log
================================================
FILE: pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I'm Service C and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
FROM alpine
COPY --from=build /log /log
================================================
FILE: pkg/e2e/fixtures/build-test/privileged/Dockerfile
================================================
# syntax = docker/dockerfile:experimental
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
RUN --security=insecure cat /proc/self/status | grep CapEff
================================================
FILE: pkg/e2e/fixtures/build-test/privileged/compose.yaml
================================================
services:
privileged-service:
build:
context: .
privileged: true
================================================
FILE: pkg/e2e/fixtures/build-test/profiles/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
RUN --mount=type=secret,id=test-secret ls -la /run/secrets/; cp /run/secrets/test-secret /tmp
CMD ["cat", "/tmp/test-secret"]
================================================
FILE: pkg/e2e/fixtures/build-test/profiles/compose.yaml
================================================
secrets:
test-secret:
file: test-secret.txt
services:
secret-build-test:
profiles: ["test"]
build:
context: .
dockerfile: Dockerfile
secrets:
- test-secret
================================================
FILE: pkg/e2e/fixtures/build-test/profiles/test-secret.txt
================================================
SECRET
================================================
FILE: pkg/e2e/fixtures/build-test/secrets/Dockerfile
================================================
# syntax=docker/dockerfile:1
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
RUN echo "foo" > /tmp/expected
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret > /tmp/actual
RUN diff /tmp/expected /tmp/actual
RUN echo "bar" > /tmp/expected
RUN --mount=type=secret,id=build_secret cat /run/secrets/build_secret > tmp/actual
RUN diff --ignore-all-space /tmp/expected /tmp/actual
RUN echo "zot" > /tmp/expected
RUN --mount=type=secret,id=dotenvsecret cat /run/secrets/dotenvsecret > tmp/actual
RUN diff --ignore-all-space /tmp/expected /tmp/actual
================================================
FILE: pkg/e2e/fixtures/build-test/secrets/compose.yml
================================================
services:
ssh:
image: build-test-secret
build:
context: .
secrets:
- mysecret
- dotenvsecret
- source: envsecret
target: build_secret
secrets:
mysecret:
file: ./secret.txt
envsecret:
environment: SOME_SECRET
dotenvsecret:
environment: ANOTHER_SECRET
================================================
FILE: pkg/e2e/fixtures/build-test/secrets/secret.txt
================================================
foo
================================================
FILE: pkg/e2e/fixtures/build-test/ssh/Dockerfile
================================================
# syntax=docker/dockerfile:1
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
RUN apk add --no-cache openssh-client
WORKDIR /compose
COPY fake_rsa.pub /compose/
RUN --mount=type=ssh,id=fake-ssh,required=true diff <(ssh-add -L) <(cat /compose/fake_rsa.pub)
================================================
FILE: pkg/e2e/fixtures/build-test/ssh/compose-without-ssh.yaml
================================================
services:
ssh:
image: build-test-ssh
build:
context: .
================================================
FILE: pkg/e2e/fixtures/build-test/ssh/compose.yaml
================================================
services:
ssh:
image: build-test-ssh
build:
context: .
ssh:
- fake-ssh=./fake_rsa
================================================
FILE: pkg/e2e/fixtures/build-test/ssh/fake_rsa
================================================
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAgEA7nJ4xAhJ7VwI63tuay3DCHaTXeEY92H6YNZ8ptAIBY0mUn6Gc9ms
94HvcAKemCJkO0fy6U2JOoST+q1YPAJf86NrIU41hZdzrw2QdqG/A3ja4VTAaOJbH9wafK
HpWLs6kyigGti3KSBabm4HARU8lgtRE6AuCC1+mw821FzTsMWMxRp/rKVxgsiMUsdd57WR
KOdn8TRm6NHcEsy7X7zAJ7+Ch/muGGCCk3Z9+YUzoVVtY/wGYmWXXj/NUzxnEq0XLyO8HC
+QU/9dWlh1OLmoMuxN1lYtHRFWWstCboKNsOcIiJsLKfQ1t4z4jXq5P7JTLE5Pngemrr4x
K21RFjVaGQpOjyQgZn1o0wAvy78KORwgN0Elwcb/XIKJepzzezCIyXlSafeXuHP+oMjM2s
2MXNHlMKv6Jwh4QYwUQ61+bAcPkcmIdltiAMNLcxYiqEud85EQQl9ciuhMKa0bZl1OEILw
VSIasEu9BEKVrz52ZZVLGMchqOV/4f1PqPEagnfnRYEttJ6AuaYUaJXvSQP6Zj4AFb6WrP
wEBIFOuAH9i4WtG52QAK6uc1wsPZlHm8J+VnTEBKFuGERu/uJBWPo43Lju8VrHuZU8QeON
ERKfJbc1EI9XpqWi+3VcWT0QJtxEGW2YmD505+cKNc31xwOtcqwogtwT0wnuj0BAf33HY3
8AAAc465v1nOub9ZwAAAAHc3NoLXJzYQAAAgEA7nJ4xAhJ7VwI63tuay3DCHaTXeEY92H6
YNZ8ptAIBY0mUn6Gc9ms94HvcAKemCJkO0fy6U2JOoST+q1YPAJf86NrIU41hZdzrw2Qdq
G/A3ja4VTAaOJbH9wafKHpWLs6kyigGti3KSBabm4HARU8lgtRE6AuCC1+mw821FzTsMWM
xRp/rKVxgsiMUsdd57WRKOdn8TRm6NHcEsy7X7zAJ7+Ch/muGGCCk3Z9+YUzoVVtY/wGYm
WXXj/NUzxnEq0XLyO8HC+QU/9dWlh1OLmoMuxN1lYtHRFWWstCboKNsOcIiJsLKfQ1t4z4
jXq5P7JTLE5Pngemrr4xK21RFjVaGQpOjyQgZn1o0wAvy78KORwgN0Elwcb/XIKJepzzez
CIyXlSafeXuHP+oMjM2s2MXNHlMKv6Jwh4QYwUQ61+bAcPkcmIdltiAMNLcxYiqEud85EQ
Ql9ciuhMKa0bZl1OEILwVSIasEu9BEKVrz52ZZVLGMchqOV/4f1PqPEagnfnRYEttJ6Aua
YUaJXvSQP6Zj4AFb6WrPwEBIFOuAH9i4WtG52QAK6uc1wsPZlHm8J+VnTEBKFuGERu/uJB
WPo43Lju8VrHuZU8QeONERKfJbc1EI9XpqWi+3VcWT0QJtxEGW2YmD505+cKNc31xwOtcq
wogtwT0wnuj0BAf33HY38AAAADAQABAAACAGK7A0YoKHQfp5HZid7XE+ptLpewnKXR69os
9XAcszWZPETsHr/ZYcUaCApZC1Hy642gPPRdJnUUcDFblS1DzncTM0iXGZI3I69X7nkwf+
bwI7EpZoIHN7P5bv4sDHKxE4/bQm/bS/u7abZP2JaaNHvsM6XsrSK1s7aAljNYPE71fVQf
pL3Xwyhj4bZk1n0asQA+0MsO541/V6BxJSR/AxFyOpoSyANP8sEcTw0CGl6zAJhlwj770b
E0uc+9MvCIuxDJuxnwl9Iv6nd+KQtT1FFBhvk4tXVTuG3fu6IGbKTTBLWLfRPiClv2AvSR
3CKDs+ykgFLu2BWCqtlQakLH1IW9DTkPExV4ZjkGCRWHEvmJxxOqL6B48tBjwa5gBuPJRA
aYRi15Z3sprsqCBfp+aHPkMXkkNGSe5ROj8lFFY/f50ZS/9DSlyuUURFLtIGe5XuPNJk7L
xJkYJAdNbgvk4IPgzsU2EuYvSja5mtuo3dVyEIRtsIAN4xl01edDAxHEow6ar4gZCtXnBb
WqeqchEi4zVTdkkuDP3SF362pktdY7Op0mS/yFd8LFrca3VCy2PqNhKvlxClRqM9Tlp9cY
qDuyS9AGT1QO4BMtvSJGFa3P+h76rQsNldC+nGa4wNWvpAUcT5NS8W9QnGp7ah/qOK07t7
fwYYENeRaAK3OItBABAAABAFjyDlnERaZ+/23B+zN0kQhCvmiNS5HE2+ooR5ofX08F3Uar
VPevy9p6s2LA+AlXY1ZZ1k0p5MI+4TkAbcB/VXaxrRUw9633p9rAgyumFGhK3i0M4whOCO
MJxmlp5sz5Qea+YzIa9z0F4ZwwvdHt7cp5joYBZoQ+Kv9OUy4xCs1zZ4ZbEsakGBrtLiTo
H3odXSg0mXQf10Ae3WkvAJ8M1xL/z1ryFeCvyv1sGwEx+5gvmZ6nnuJEEuXUBlpOwhPlST
4X9VL7gmdH9OoHnhUn3q2JEBQdVTegGij9wvoYT1bdzwBN/Amisn29K9w1aNdrNbYUJ6PO
0kE2lotSJ11qD8MAAAEBAP6IRuU25yj7zv0mEsaRWoQ5v3fYKKn4C6Eg3DbzKXybZkLyX7
6QlyO7uWf54SdXM7sQW8KoXaMu9qbo/o+4o3m7YfOY1MYeTz3yICYObVA7Fc9ZHwKzc1PB
dFNzy6/G+2niNQF3Q1Fjp31Ve9LwKJK8Kj/eUYZ3QiUIropkw4ppA8q3h+nkVGS23xSrTM
kGLugBjcnWUfuN0tKx/b5mqziRoyzr5u0qzFDtx97QAyETo/onFrd1bMGED2BHVyrCwtqI
p6SXo2uFzwm/nLtOMlmfpixNcK6dtql/brx3Lsu18a+0a42O5Q/TYRdRq8D60O16rUS/LN
sFOjIYSA3spnUAAAEBAO/Sc3NTarFylk+yhOTE8G9xDt5ndbY0gsfhM9D4byKlY4yYIvs+
yQAq3UHgSoN2f087zNubXSNiLJ8TOIPpbk8MzdvjqcpmnBhHcd4V2FLe9+hC8zEBf8MPPf
Cy1kXdCZ0bDMLTdgONiDTIc/0YXhFLZherXNIF1o/7Pcnu6IPwMDl/gcG3H1ncDxaLqxAm
L29SDXLX2hH9k+YJr9kFaho7PZBAwNYnMooupROSbQ9/lmfCt09ep/83n5G0mo93uGkyV2
1wcQw9X2ZT8eVHZ4ni3ACC6VYbUn2M3Z+e3tpGaYzKXd/yq0YyppoRvEaxM/ewXappUJul
Xsd/RqSc66MAAAAAAQID
-----END OPENSSH PRIVATE KEY-----
================================================
FILE: pkg/e2e/fixtures/build-test/ssh/fake_rsa.pub
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDucnjECEntXAjre25rLcMIdpNd4Rj3Yfpg1nym0AgFjSZSfoZz2az3ge9wAp6YImQ7R/LpTYk6hJP6rVg8Al/zo2shTjWFl3OvDZB2ob8DeNrhVMBo4lsf3Bp8oelYuzqTKKAa2LcpIFpubgcBFTyWC1EToC4ILX6bDzbUXNOwxYzFGn+spXGCyIxSx13ntZEo52fxNGbo0dwSzLtfvMAnv4KH+a4YYIKTdn35hTOhVW1j/AZiZZdeP81TPGcSrRcvI7wcL5BT/11aWHU4uagy7E3WVi0dEVZay0Jugo2w5wiImwsp9DW3jPiNerk/slMsTk+eB6auvjErbVEWNVoZCk6PJCBmfWjTAC/Lvwo5HCA3QSXBxv9cgol6nPN7MIjJeVJp95e4c/6gyMzazYxc0eUwq/onCHhBjBRDrX5sBw+RyYh2W2IAw0tzFiKoS53zkRBCX1yK6EwprRtmXU4QgvBVIhqwS70EQpWvPnZllUsYxyGo5X/h/U+o8RqCd+dFgS20noC5phRole9JA/pmPgAVvpas/AQEgU64Af2Lha0bnZAArq5zXCw9mUebwn5WdMQEoW4YRG7+4kFY+jjcuO7xWse5lTxB440REp8ltzUQj1empaL7dVxZPRAm3EQZbZiYPnTn5wo1zfXHA61yrCiC3BPTCe6PQEB/fcdjfw==
================================================
FILE: pkg/e2e/fixtures/build-test/sub-dependencies/compose.yaml
================================================
services:
main:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
dep2: service:dep2
entrypoint: ["echo", "Hello from main"]
dep1:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
subdep1: service:subdep1
subdep2: service:subdep2
entrypoint: ["echo", "Hello from dep1"]
dep2:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep2"]
subdep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from subdep1"]
subdep2:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from subdep2"]
================================================
FILE: pkg/e2e/fixtures/build-test/subset/compose.yaml
================================================
services:
main:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from main"]
depends_on:
- dep1
dep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep1"]
================================================
FILE: pkg/e2e/fixtures/build-test/tags/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
RUN echo "SUCCESS"
================================================
FILE: pkg/e2e/fixtures/build-test/tags/compose.yaml
================================================
services:
nginx:
image: build-test-tags
build:
context: .
tags:
- docker.io/docker/build-test-tags:1.0.0
- other-image-name:v1.0.0
================================================
FILE: pkg/e2e/fixtures/cascade/compose.yaml
================================================
services:
running:
image: alpine
command: sleep infinity
init: true
exit:
image: alpine
command: /bin/true
depends_on:
running:
condition: service_started
fail:
image: alpine
command: sh -c "return 111"
depends_on:
exit:
condition: service_completed_successfully
================================================
FILE: pkg/e2e/fixtures/commit/compose.yaml
================================================
services:
service:
image: alpine
command: sleep infinity
service-with-replicas:
image: alpine
command: sleep infinity
deploy:
replicas: 3
================================================
FILE: pkg/e2e/fixtures/compose-pull/duplicate-images/compose.yaml
================================================
services:
simple:
image: alpine:3.13
command: top
another:
image: alpine:3.13
command: top
================================================
FILE: pkg/e2e/fixtures/compose-pull/image-present-locally/compose.yaml
================================================
services:
simple:
image: alpine:3.13.12
pull_policy: missing
command: top
latest:
image: alpine:latest
pull_policy: missing
command: top
================================================
FILE: pkg/e2e/fixtures/compose-pull/no-image-name-given/compose.yaml
================================================
services:
no-image-service:
build: .
================================================
FILE: pkg/e2e/fixtures/compose-pull/simple/compose.yaml
================================================
services:
simple:
image: alpine:3.14
command: top
another:
image: alpine:3.15
command: top
================================================
FILE: pkg/e2e/fixtures/compose-pull/unknown-image/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine:3.15
================================================
FILE: pkg/e2e/fixtures/compose-pull/unknown-image/compose.yaml
================================================
services:
fail:
image: does_not_exists
can_build:
image: doesn_t_exists_either
build: .
valid:
image: alpine:3.15
================================================
FILE: pkg/e2e/fixtures/config/compose.yaml
================================================
services:
test:
image: test
ports:
- ${PORT:-8080}:80
================================================
FILE: pkg/e2e/fixtures/configs/compose.yaml
================================================
services:
from_env:
image: alpine
configs:
- source: from_env
command: cat /from_env
from_file:
image: alpine
configs:
- source: from_file
command: cat /from_file
inlined:
image: alpine
configs:
- source: inlined
command: cat /inlined
target:
image: alpine
configs:
- source: inlined
target: /target
command: cat /target
configs:
from_env:
environment: CONFIG
from_file:
file: config.txt
inlined:
content: This is my $CONFIG
================================================
FILE: pkg/e2e/fixtures/configs/config.txt
================================================
This is my config file
================================================
FILE: pkg/e2e/fixtures/container_name/compose.yaml
================================================
services:
test:
image: alpine
container_name: test
command: /bin/true
another_test:
image: alpine
container_name: test
command: /bin/true
================================================
FILE: pkg/e2e/fixtures/cp-test/compose.yaml
================================================
services:
nginx:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/cp-test/cp-folder/cp-me.txt
================================================
hello world from folder
================================================
FILE: pkg/e2e/fixtures/cp-test/cp-me.txt
================================================
hello world
================================================
FILE: pkg/e2e/fixtures/dependencies/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM busybox:1.35.0
RUN echo "hello"
================================================
FILE: pkg/e2e/fixtures/dependencies/compose.yaml
================================================
services:
foo:
image: nginx:alpine
command: "${COMMAND}"
depends_on:
- bar
bar:
image: nginx:alpine
scale: 2
================================================
FILE: pkg/e2e/fixtures/dependencies/dependency-exit.yaml
================================================
services:
web:
image: nginx:alpine
depends_on:
db:
condition: service_healthy
db:
image: alpine
command: sh -c "exit 1"
================================================
FILE: pkg/e2e/fixtures/dependencies/deps-completed-successfully.yaml
================================================
services:
oneshot:
image: alpine
command: echo 'hello world'
longrunning:
image: alpine
init: true
depends_on:
oneshot:
condition: service_completed_successfully
command: sleep infinity
================================================
FILE: pkg/e2e/fixtures/dependencies/deps-not-required.yaml
================================================
services:
foo:
image: bash
command: echo "foo"
depends_on:
bar:
required: false
condition: service_healthy
bar:
image: nginx:alpine
profiles: [not-required]
================================================
FILE: pkg/e2e/fixtures/dependencies/recreate-no-deps.yaml
================================================
version: '3.8'
services:
my-service:
image: alpine
command: tail -f /dev/null
init: true
depends_on:
nginx: {condition: service_healthy}
nginx:
image: nginx:alpine
stop_signal: SIGTERM
healthcheck:
test: "echo | nc -w 5 localhost:80"
interval: 2s
timeout: 1s
retries: 10
================================================
FILE: pkg/e2e/fixtures/dependencies/service-image-depends-on.yaml
================================================
services:
foo:
image: built-image-dependency
build:
context: .
bar:
image: built-image-dependency
depends_on:
- foo
================================================
FILE: pkg/e2e/fixtures/dotenv/development/compose.yaml
================================================
services:
backend:
image: $IMAGE_NAME:$IMAGE_TAG
test:
profiles:
- test
image: enabled:profile
================================================
FILE: pkg/e2e/fixtures/dotenv/raw.yaml
================================================
services:
test:
image: alpine
command: sh -c "echo $$TEST_VAR"
env_file:
- path: .env.raw
format: raw # parse without interpolation
================================================
FILE: pkg/e2e/fixtures/env-secret/child/compose.yaml
================================================
services:
included:
image: alpine
secrets:
- my-secret
command: cat /run/secrets/my-secret
secrets:
my-secret:
environment: 'MY_SECRET'
================================================
FILE: pkg/e2e/fixtures/env-secret/compose.yaml
================================================
include:
- path: child/compose.yaml
env_file:
- secret.env
services:
foo:
image: alpine
secrets:
- source: secret
target: bar
uid: "1005"
gid: "1005"
mode: 0440
command: cat /run/secrets/bar
secrets:
secret:
environment: SECRET
================================================
FILE: pkg/e2e/fixtures/env-secret/secret.env
================================================
MY_SECRET='this-is-secret'
================================================
FILE: pkg/e2e/fixtures/env_file/compose.yaml
================================================
services:
serviceA:
image: nginx:latest
serviceB:
image: nginx:latest
env_file:
- /doesnotexist/.env
serviceC:
profiles: ["test"]
image: alpine
env_file: test.env
================================================
FILE: pkg/e2e/fixtures/env_file/test.env
================================================
FOO=BAR
================================================
FILE: pkg/e2e/fixtures/environment/empty-variable/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
ENV EMPTY=not_empty
CMD ["sh", "-c", "echo \"=$EMPTY=\""]
================================================
FILE: pkg/e2e/fixtures/environment/empty-variable/compose.yaml
================================================
services:
empty-variable:
build:
context: .
image: empty-variable
environment:
- EMPTY # expect to propagate value from user's env OR unset in container
================================================
FILE: pkg/e2e/fixtures/environment/env-file-comments/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
ENV COMMENT=Dockerfile
ENV NO_COMMENT=Dockerfile
CMD ["sh", "-c", "printenv", "|", "grep", "COMMENT"]
================================================
FILE: pkg/e2e/fixtures/environment/env-file-comments/compose.yaml
================================================
services:
env-file-comments:
build:
context: .
image: env-file-comments
================================================
FILE: pkg/e2e/fixtures/environment/env-interpolation/compose.yaml
================================================
services:
env-interpolation:
image: bash
environment:
IMAGE: ${IMAGE}
command: echo "$IMAGE"
================================================
FILE: pkg/e2e/fixtures/environment/env-interpolation-default-value/compose.yaml
================================================
services:
env-interpolation:
image: bash
environment:
IMAGE: ${IMAGE}
command: echo "$IMAGE"
================================================
FILE: pkg/e2e/fixtures/environment/env-priority/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine
ENV WHEREAMI=Dockerfile
CMD ["printenv", "WHEREAMI"]
================================================
FILE: pkg/e2e/fixtures/environment/env-priority/compose-with-env-file.yaml
================================================
services:
env-compose-priority:
image: env-compose-priority
build:
context: .
env_file:
- .env.override
================================================
FILE: pkg/e2e/fixtures/environment/env-priority/compose-with-env.yaml
================================================
services:
env-compose-priority:
image: env-compose-priority
build:
context: .
environment:
WHEREAMI: "Compose File"
================================================
FILE: pkg/e2e/fixtures/environment/env-priority/compose.yaml
================================================
services:
env-compose-priority:
image: env-compose-priority
build:
context: .
================================================
FILE: pkg/e2e/fixtures/exec/compose.yaml
================================================
services:
test:
image: alpine
command: tail -f /dev/null
================================================
FILE: pkg/e2e/fixtures/export/compose.yaml
================================================
services:
service:
image: alpine
command: sleep infinity
service-with-replicas:
image: alpine
command: sleep infinity
deploy:
replicas: 3
================================================
FILE: pkg/e2e/fixtures/external/compose.yaml
================================================
services:
test:
image: nginx:alpine
other:
image: nginx:alpine
networks:
test_network:
ipv4_address: 8.8.8.8
networks:
test_network:
external: true
name: foo_bar
================================================
FILE: pkg/e2e/fixtures/hooks/compose.yaml
================================================
services:
sample:
image: nginx
volumes:
- data:/data
pre_stop:
- command: sh -c 'echo "In the pre-stop" >> /data/log.txt'
test:
image: nginx
post_start:
- command: sh -c 'echo env'
volumes:
data:
name: sample-data
================================================
FILE: pkg/e2e/fixtures/hooks/poststart/compose-error.yaml
================================================
services:
test:
image: nginx
post_start:
- command: sh -c 'command in error'
================================================
FILE: pkg/e2e/fixtures/hooks/poststart/compose-success.yaml
================================================
services:
test:
image: nginx
post_start:
- command: sh -c 'echo env'
================================================
FILE: pkg/e2e/fixtures/hooks/prestop/compose-error.yaml
================================================
services:
sample:
image: nginx
volumes:
- data:/data
pre_stop:
- command: sh -c 'command in error'
volumes:
data:
name: sample-data
================================================
FILE: pkg/e2e/fixtures/hooks/prestop/compose-success.yaml
================================================
services:
sample:
image: nginx
volumes:
- data:/data
pre_stop:
- command: sh -c 'echo "In the pre-stop" >> /data/log.txt'
volumes:
data:
name: sample-data
================================================
FILE: pkg/e2e/fixtures/image-volume-recreate/Dockerfile
================================================
# syntax=docker/dockerfile:1
#
# Copyright 2020 Docker Compose CLI authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM alpine
WORKDIR /app
ARG CONTENT=initial
RUN echo "$CONTENT" > /app/content.txt
================================================
FILE: pkg/e2e/fixtures/image-volume-recreate/compose.yaml
================================================
services:
source:
build:
context: .
dockerfile: Dockerfile
image: image-volume-source
consumer:
image: alpine
depends_on:
- source
command: ["cat", "/data/content.txt"]
volumes:
- type: image
source: image-volume-source
target: /data
image:
subpath: app
================================================
FILE: pkg/e2e/fixtures/init-container/compose.yaml
================================================
services:
foo:
image: alpine
command: "echo hello"
bar:
image: alpine
command: "echo world"
depends_on:
foo:
condition: "service_completed_successfully"
================================================
FILE: pkg/e2e/fixtures/ipam/compose.yaml
================================================
services:
foo:
image: alpine
init: true
entrypoint: ["sleep", "600"]
networks:
default:
ipv4_address: 10.1.0.100 # <-- Fixed IP address
networks:
default:
ipam:
config:
- subnet: 10.1.0.0/16
================================================
FILE: pkg/e2e/fixtures/ipc-test/compose.yaml
================================================
services:
service:
image: alpine
command: top
ipc: "service:shareable"
container:
image: alpine
command: top
ipc: "container:ipc_mode_container"
shareable:
image: alpine
command: top
ipc: shareable
================================================
FILE: pkg/e2e/fixtures/links/compose.yaml
================================================
services:
foo:
image: nginx:alpine
links:
- bar
bar:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/logging-driver/compose.yaml
================================================
services:
fluentbit:
image: fluent/fluent-bit:3.1.7-debug
ports:
- "24224:24224"
- "24224:24224/udp"
environment:
FOO: ${BAR}
app:
image: nginx
depends_on:
fluentbit:
condition: service_started
restart: true
logging:
driver: fluentd
options:
fluentd-address: ${HOST:-127.0.0.1}:24224
================================================
FILE: pkg/e2e/fixtures/logs-test/cat.yaml
================================================
services:
test:
image: alpine
command: cat /text_file.txt
volumes:
- ${FILE}:/text_file.txt
================================================
FILE: pkg/e2e/fixtures/logs-test/compose.yaml
================================================
services:
ping:
image: alpine
init: true
command: ping localhost -c ${REPEAT:-1}
hello:
image: alpine
command: echo hello
deploy:
replicas: 2
================================================
FILE: pkg/e2e/fixtures/logs-test/restart.yaml
================================================
services:
ping:
image: alpine
command: "sh -c 'ping -c 2 localhost && exit 1'"
restart: "on-failure:2"
================================================
FILE: pkg/e2e/fixtures/model/compose.yaml
================================================
services:
test:
image: alpine/curl
models:
- foo
models:
foo:
model: ai/smollm2
================================================
FILE: pkg/e2e/fixtures/nested/compose.yaml
================================================
services:
echo:
image: alpine
command: echo $ROOT $SUB win=$WIN
================================================
FILE: pkg/e2e/fixtures/network-alias/compose.yaml
================================================
services:
container1:
image: nginx
links:
- container2:container
container2:
image: nginx
networks:
default:
aliases:
- alias-of-container2
================================================
FILE: pkg/e2e/fixtures/network-interface-name/compose.yaml
================================================
services:
test:
image: alpine
command: ip link show
networks:
default:
interface_name: foobar
================================================
FILE: pkg/e2e/fixtures/network-links/compose.yaml
================================================
services:
container1:
image: nginx
network_mode: bridge
container2:
image: nginx
network_mode: bridge
links:
- container1
================================================
FILE: pkg/e2e/fixtures/network-recreate/compose.yaml
================================================
services:
web:
image: nginx
networks:
- test
networks:
test:
labels:
- foo=${FOO:-foo}
================================================
FILE: pkg/e2e/fixtures/network-test/compose.subnet.yaml
================================================
services:
test:
image: nginx:alpine
networks:
- test
networks:
test:
ipam:
config:
- subnet: ${SUBNET-172.99.0.0/16}
================================================
FILE: pkg/e2e/fixtures/network-test/compose.yaml
================================================
services:
mydb:
image: mariadb
network_mode: "service:db"
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
db:
image: gtardif/sentences-db
init: true
networks:
- dbnet
- closesnetworkname1
- closesnetworkname2
words:
image: gtardif/sentences-api
init: true
ports:
- "8080:8080"
networks:
- dbnet
- servicenet
web:
image: gtardif/sentences-web
init: true
ports:
- "80:80"
labels:
- "my-label=test"
networks:
- servicenet
networks:
dbnet:
servicenet:
name: microservices
closesnetworkname1:
name: closenamenet
closesnetworkname2:
name: closenamenet-2
================================================
FILE: pkg/e2e/fixtures/network-test/mac_address.yaml
================================================
services:
test:
image: nginx:alpine
mac_address: 00:e0:84:35:d0:e8
================================================
FILE: pkg/e2e/fixtures/no-deps/network-mode.yaml
================================================
services:
app:
image: nginx:alpine
network_mode: service:db
db:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/no-deps/volume-from.yaml
================================================
services:
app:
image: nginx:alpine
volumes_from:
- db
db:
image: nginx:alpine
volumes:
- /var/data
================================================
FILE: pkg/e2e/fixtures/orphans/compose.yaml
================================================
services:
orphan:
profiles: [run]
image: alpine
command: echo hello
test:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/pause/compose.yaml
================================================
services:
a:
image: nginx:alpine
ports: [80]
healthcheck:
test: wget --spider -S -T1 http://localhost:80
interval: 1s
timeout: 1s
b:
image: nginx:alpine
ports: [80]
depends_on:
- a
healthcheck:
test: wget --spider -S -T1 http://localhost:80
interval: 1s
timeout: 1s
================================================
FILE: pkg/e2e/fixtures/port-range/compose.yaml
================================================
services:
a:
image: nginx:alpine
scale: 5
ports:
- "6005-6015:80"
b:
image: nginx:alpine
ports:
- 80
c:
image: nginx:alpine
ports:
- 80
================================================
FILE: pkg/e2e/fixtures/profiles/compose.yaml
================================================
services:
regular-service:
image: nginx:alpine
profiled-service:
image: nginx:alpine
profiles:
- test-profile
================================================
FILE: pkg/e2e/fixtures/profiles/docker-compose.yaml
================================================
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
================================================
FILE: pkg/e2e/fixtures/profiles/test-profile.env
================================================
COMPOSE_PROFILES=test-profile
================================================
FILE: pkg/e2e/fixtures/project-volume-bind-test/docker-compose.yml
================================================
services:
frontend:
image: nginx
container_name: frontend
volumes:
- project-data:/data
volumes:
project-data:
driver: local
driver_opts:
type: none
o: bind
device: "${TEST_DIR}"
================================================
FILE: pkg/e2e/fixtures/providers/depends-on-multiple-providers.yaml
================================================
services:
test:
image: alpine
command: env
depends_on:
- provider1
- provider2
provider1:
provider:
type: example-provider
options:
name: provider1
type: test1
size: 1
provider2:
provider:
type: example-provider
options:
name: provider2
type: test2
size: 2
================================================
FILE: pkg/e2e/fixtures/ps-test/compose.yaml
================================================
services:
nginx:
image: nginx:latest
expose:
- '80'
- '443'
- '8080'
busybox:
image: busybox
command: busybox httpd -f -p 8000
ports:
- '127.0.0.1:8001:8000'
================================================
FILE: pkg/e2e/fixtures/publish/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine:latest
================================================
FILE: pkg/e2e/fixtures/publish/common.yaml
================================================
services:
foo:
image: bar
================================================
FILE: pkg/e2e/fixtures/publish/compose-bind-mount.yml
================================================
services:
serviceA:
image: a
volumes:
- .:/user-data
================================================
FILE: pkg/e2e/fixtures/publish/compose-build-only.yml
================================================
services:
serviceA:
build:
context: .
dockerfile: Dockerfile
serviceB:
build:
context: .
dockerfile: Dockerfile
================================================
FILE: pkg/e2e/fixtures/publish/compose-env-file.yml
================================================
services:
serviceA:
image: "alpine:3.12"
env_file:
- publish.env
serviceB:
image: "alpine:3.12"
================================================
FILE: pkg/e2e/fixtures/publish/compose-environment.yml
================================================
services:
serviceA:
image: "alpine:3.12"
environment:
- "FOO=bar"
serviceB:
image: "alpine:3.12"
================================================
FILE: pkg/e2e/fixtures/publish/compose-local-include.yml
================================================
include:
- common.yaml
services:
test:
image: test
================================================
FILE: pkg/e2e/fixtures/publish/compose-multi-env-config.yml
================================================
services:
serviceA:
image: "alpine:3.12"
environment:
- "FOO=bar"
serviceB:
image: "alpine:3.12"
env_file:
- publish.env
environment:
- "BAR=baz"
================================================
FILE: pkg/e2e/fixtures/publish/compose-sensitive.yml
================================================
services:
serviceA:
image: "alpine:3.12"
environment:
- AWS_ACCESS_KEY_ID=A3TX1234567890ABCDEF
- AWS_SECRET_ACCESS_KEY=aws"12345+67890/abcdefghijklm+NOPQRSTUVWXYZ+"
configs:
- myconfig
serviceB:
image: "alpine:3.12"
env_file:
- publish-sensitive.env
secrets:
- mysecret
configs:
myconfig:
file: config.txt
secrets:
mysecret:
file: secret.txt
================================================
FILE: pkg/e2e/fixtures/publish/compose-with-extends.yml
================================================
services:
test:
extends:
file: common.yaml
service: foo
================================================
FILE: pkg/e2e/fixtures/publish/config.txt
================================================
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw
================================================
FILE: pkg/e2e/fixtures/publish/oci/compose-override.yaml
================================================
services:
app:
env_file: test.env
================================================
FILE: pkg/e2e/fixtures/publish/oci/compose.yaml
================================================
services:
app:
extends:
file: extends.yaml
service: test
================================================
FILE: pkg/e2e/fixtures/publish/oci/extends.yaml
================================================
services:
test:
image: alpine
================================================
FILE: pkg/e2e/fixtures/publish/oci/test.env
================================================
HELLO=WORLD
================================================
FILE: pkg/e2e/fixtures/publish/publish-sensitive.env
================================================
GITHUB_TOKEN=ghp_1234567890abcdefghijklmnopqrstuvwxyz
================================================
FILE: pkg/e2e/fixtures/publish/publish.env
================================================
FOO=bar
QUIX=
================================================
FILE: pkg/e2e/fixtures/publish/secret.txt
================================================
-----BEGIN DSA PRIVATE KEY-----
wxyz+ABC=
-----END DSA PRIVATE KEY-----
================================================
FILE: pkg/e2e/fixtures/recreate-volumes/bind.yaml
================================================
services:
app:
image: alpine
volumes:
- .:/my_vol
================================================
FILE: pkg/e2e/fixtures/recreate-volumes/compose.yaml
================================================
services:
app:
image: alpine
volumes:
- my_vol:/my_vol
volumes:
my_vol:
labels:
foo: bar
================================================
FILE: pkg/e2e/fixtures/recreate-volumes/compose2.yaml
================================================
services:
app:
image: alpine
volumes:
- my_vol:/my_vol
volumes:
my_vol:
labels:
foo: zot
================================================
FILE: pkg/e2e/fixtures/resources/compose.yaml
================================================
volumes:
my_vol: {}
networks:
my_net: {}
================================================
FILE: pkg/e2e/fixtures/restart-test/compose-depends-on.yaml
================================================
services:
with-restart:
image: nginx:alpine
init: true
command: tail -f /dev/null
stop_signal: SIGTERM
depends_on:
nginx: {condition: service_healthy, restart: true}
no-restart:
image: nginx:alpine
init: true
command: tail -f /dev/null
stop_signal: SIGTERM
depends_on:
nginx: { condition: service_healthy }
nginx:
image: nginx:alpine
labels:
TEST: ${LABEL:-test}
stop_signal: SIGTERM
healthcheck:
test: "echo | nc -w 5 localhost:80"
interval: 2s
timeout: 1s
retries: 10
================================================
FILE: pkg/e2e/fixtures/restart-test/compose.yaml
================================================
services:
restart:
image: alpine
init: true
command: ash -c "if [[ -f /tmp/restart.lock ]] ; then sleep infinity; else touch /tmp/restart.lock; fi"
test:
profiles:
- test
image: alpine
init: true
command: ash -c "if [[ -f /tmp/restart.lock ]] ; then sleep infinity; else touch /tmp/restart.lock; fi"
================================================
FILE: pkg/e2e/fixtures/run-test/build-once-nested.yaml
================================================
services:
# Database service with build
db:
pull_policy: build
build:
dockerfile_inline: |
FROM alpine
RUN echo "DB built at $(date)" > /db-build.txt
CMD sleep 3600
# API service that depends on db
api:
pull_policy: build
build:
dockerfile_inline: |
FROM alpine
RUN echo "API built at $(date)" > /api-build.txt
CMD sleep 3600
depends_on:
- db
# App service that depends on api (which depends on db)
app:
pull_policy: build
build:
dockerfile_inline: |
FROM alpine
RUN echo "App built at $(date)" > /app-build.txt
CMD echo "App running"
depends_on:
- api
================================================
FILE: pkg/e2e/fixtures/run-test/build-once-no-deps.yaml
================================================
services:
# Simple service with no dependencies
simple:
pull_policy: build
build:
dockerfile_inline: |
FROM alpine
RUN echo "Simple built at $(date)" > /build.txt
CMD echo "Simple service"
================================================
FILE: pkg/e2e/fixtures/run-test/build-once.yaml
================================================
services:
# Service with pull_policy: build to ensure it always rebuilds
# This is the key to testing the bug - without the fix, this would build twice
nginx:
pull_policy: build
build:
dockerfile_inline: |
FROM alpine
RUN echo "Nginx built at $(date)" > /build-time.txt
CMD sleep 3600
# Service that depends on nginx
curl:
image: alpine
depends_on:
- nginx
command: echo "curl service"
================================================
FILE: pkg/e2e/fixtures/run-test/compose.yaml
================================================
services:
back:
image: alpine
command: echo "Hello there!!"
depends_on:
- db
networks:
- backnet
db:
image: nginx:alpine
networks:
- backnet
volumes:
- data:/test
front:
image: nginx:alpine
networks:
- frontnet
build:
build:
dockerfile_inline: "FROM base"
additional_contexts:
base: "service:build_base"
build_base:
build:
dockerfile_inline: "FROM alpine"
networks:
frontnet:
backnet:
volumes:
data:
================================================
FILE: pkg/e2e/fixtures/run-test/deps.yaml
================================================
services:
service_a:
image: bash
command: echo "a"
depends_on:
- shared_dep
service_b:
image: bash
command: echo "b"
depends_on:
- shared_dep
shared_dep:
image: bash
================================================
FILE: pkg/e2e/fixtures/run-test/orphan.yaml
================================================
services:
simple:
image: alpine
command: echo "Hi there!!"
================================================
FILE: pkg/e2e/fixtures/run-test/piped-test.yaml
================================================
services:
piped-test:
image: alpine
command: cat
# Service that will receive piped input and echo it back
tty-test:
image: alpine
command: sh -c "if [ -t 0 ]; then echo 'TTY detected'; else echo 'No TTY detected'; fi"
# Service to test TTY detection
================================================
FILE: pkg/e2e/fixtures/run-test/ports.yaml
================================================
services:
back:
image: alpine
ports:
- 8082:80
================================================
FILE: pkg/e2e/fixtures/run-test/pull.yaml
================================================
services:
backend:
image: nginx
command: nginx -t
================================================
FILE: pkg/e2e/fixtures/run-test/quiet-pull.yaml
================================================
services:
backend:
image: hello-world
================================================
FILE: pkg/e2e/fixtures/run-test/run.env
================================================
FOO=BAR
================================================
FILE: pkg/e2e/fixtures/scale/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
ARG FOO
LABEL FOO=$FOO
================================================
FILE: pkg/e2e/fixtures/scale/build.yaml
================================================
services:
test:
build: .
================================================
FILE: pkg/e2e/fixtures/scale/compose.yaml
================================================
services:
back:
image: nginx:alpine
depends_on:
- db
db:
image: nginx:alpine
environment:
- MAYBE
front:
image: nginx:alpine
deploy:
replicas: 2
dbadmin:
image: nginx:alpine
deploy:
replicas: 0
================================================
FILE: pkg/e2e/fixtures/sentences/compose.yaml
================================================
services:
db:
image: gtardif/sentences-db
init: true
words:
image: gtardif/sentences-api
init: true
ports:
- "95:8080"
web:
image: gtardif/sentences-web
init: true
ports:
- "90:80"
labels:
- "my-label=test"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 2s
================================================
FILE: pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml
================================================
services:
nginx:
build:
context: nginx-build
dockerfile: ${MYVAR}
================================================
FILE: pkg/e2e/fixtures/simple-build-test/compose.yaml
================================================
services:
nginx:
build:
context: nginx-build
dockerfile: Dockerfile
================================================
FILE: pkg/e2e/fixtures/simple-build-test/nginx-build/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
ARG FOO
LABEL FOO=$FOO
COPY static /usr/share/nginx/html
================================================
FILE: pkg/e2e/fixtures/simple-build-test/nginx-build/static/index.html
================================================
Docker Nginx
Hello from Nginx container
================================================
FILE: pkg/e2e/fixtures/simple-composefile/compose.yaml
================================================
services:
simple:
image: alpine
command: top
another:
image: alpine
command: top
================================================
FILE: pkg/e2e/fixtures/simple-composefile/id.yaml
================================================
services:
test:
image: ${ID:?ID variable must be set}
================================================
FILE: pkg/e2e/fixtures/start-fail/compose.yaml
================================================
services:
fail:
image: alpine
init: true
command: sleep infinity
healthcheck:
test: "false"
interval: 1s
retries: 3
depends:
image: alpine
init: true
command: sleep infinity
depends_on:
fail:
condition: service_healthy
================================================
FILE: pkg/e2e/fixtures/start-fail/start-depends_on-long-lived.yaml
================================================
services:
safe:
image: 'alpine'
init: true
command: ['/bin/sh', '-c', 'sleep infinity'] # never exiting
failure:
image: 'alpine'
init: true
command: ['/bin/sh', '-c', 'sleep 1 ; echo "exiting with error" ; exit 42']
test:
image: 'alpine'
init: true
command: ['/bin/sh', '-c', 'sleep 99999 ; echo "tests are OK"'] # very long job
depends_on: [safe]
================================================
FILE: pkg/e2e/fixtures/start-stop/compose.yaml
================================================
services:
simple:
image: nginx:alpine
another:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/start-stop/other.yaml
================================================
services:
a-different-one:
image: nginx:alpine
and-another-one:
image: nginx:alpine
================================================
FILE: pkg/e2e/fixtures/start-stop/start-stop-deps.yaml
================================================
services:
another_2:
image: nginx:alpine
another:
image: nginx:alpine
depends_on:
- another_2
dep_2:
image: nginx:alpine
dep_1:
image: nginx:alpine
depends_on:
- dep_2
desired:
image: nginx:alpine
depends_on:
- dep_1
================================================
FILE: pkg/e2e/fixtures/start_interval/compose.yaml
================================================
services:
test:
image: "nginx"
healthcheck:
interval: 30s
start_period: 10s
start_interval: 1s
test: "/bin/true"
error:
image: "nginx"
healthcheck:
interval: 30s
start_interval: 1s
test: "/bin/true"
================================================
FILE: pkg/e2e/fixtures/stdout-stderr/compose.yaml
================================================
services:
stderr:
image: alpine
init: true
command: /bin/ash /log_to_stderr.sh
volumes:
- ./log_to_stderr.sh:/log_to_stderr.sh
================================================
FILE: pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh
================================================
# 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.
>&2 echo "log to stderr"
echo "log to stdout"
================================================
FILE: pkg/e2e/fixtures/stop/compose.yaml
================================================
services:
service1:
image: alpine
command: /bin/true
service2:
image: alpine
command: ping -c 2 localhost
pre_stop:
- command: echo "stop hook running..."
================================================
FILE: pkg/e2e/fixtures/switch-volumes/compose.yaml
================================================
services:
app:
image: alpine
volumes:
- my_vol:/my_vol
volumes:
my_vol:
external: true
name: test_external_volume
================================================
FILE: pkg/e2e/fixtures/switch-volumes/compose2.yaml
================================================
services:
app:
image: alpine
volumes:
- my_vol:/my_vol
volumes:
my_vol:
external: true
name: test_external_volume_2
================================================
FILE: pkg/e2e/fixtures/ups-deps-stop/compose.yaml
================================================
services:
dependency:
image: alpine
init: true
command: /bin/sh -c 'while true; do echo "hello dependency"; sleep 1; done'
app:
depends_on: ['dependency']
image: alpine
init: true
command: /bin/sh -c 'while true; do echo "hello app"; sleep 1; done'
================================================
FILE: pkg/e2e/fixtures/ups-deps-stop/orphan.yaml
================================================
services:
orphan:
image: alpine
init: true
command: /bin/sh -c 'while true; do echo "hello orphan"; sleep 1; done'
================================================
FILE: pkg/e2e/fixtures/volume-test/compose.yaml
================================================
services:
nginx:
build: nginx-build
volumes:
- ./static:/usr/share/nginx/html
ports:
- 8090:80
nginx2:
build: nginx-build
volumes:
- staticVol:/usr/share/nginx/html:ro
- /usr/src/app/node_modules
- otherVol:/usr/share/nginx/test
ports:
- 9090:80
configs:
- myconfig
secrets:
- mysecret
volumes:
staticVol:
otherVol:
name: myVolume
configs:
myconfig:
file: ./static/index.html
secrets:
mysecret:
file: ./static/index.html
================================================
FILE: pkg/e2e/fixtures/volume-test/nginx-build/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:alpine
================================================
FILE: pkg/e2e/fixtures/volume-test/static/index.html
================================================
Docker Nginx
Hello from Nginx container
================================================
FILE: pkg/e2e/fixtures/volumes/compose.yaml
================================================
services:
with_image:
image: alpine
command: "ls -al /mnt/image"
volumes:
- type: image
source: nginx:alpine
target: /mnt/image
image:
subpath: usr/share/nginx/html/
================================================
FILE: pkg/e2e/fixtures/wait/compose.yaml
================================================
services:
faster:
image: alpine
command: sleep 2
slower:
image: alpine
command: sleep 5
infinity:
image: alpine
command: sleep infinity
================================================
FILE: pkg/e2e/fixtures/watch/compose.yaml
================================================
x-dev: &x-dev
watch:
- action: sync
path: ./data
target: /app/data
ignore:
- '*.foo'
- ./ignored
- action: sync+restart
path: ./config
target: /app/config
services:
alpine:
build:
dockerfile_inline: |-
FROM alpine
RUN mkdir -p /app/data
RUN mkdir -p /app/config
init: true
command: sleep infinity
develop: *x-dev
busybox:
build:
dockerfile_inline: |-
FROM busybox
RUN mkdir -p /app/data
RUN mkdir -p /app/config
init: true
command: sleep infinity
develop: *x-dev
debian:
build:
dockerfile_inline: |-
FROM debian
RUN mkdir -p /app/data
RUN mkdir -p /app/config
init: true
command: sleep infinity
volumes:
- ./dat:/app/dat
- ./data-logs:/app/data-logs
develop: *x-dev
================================================
FILE: pkg/e2e/fixtures/watch/config/file.config
================================================
This is a config file
================================================
FILE: pkg/e2e/fixtures/watch/data/hello.txt
================================================
hello world
================================================
FILE: pkg/e2e/fixtures/watch/data-logs/server.log
================================================
[INFO] Server started successfully on port 8080
================================================
FILE: pkg/e2e/fixtures/watch/exec.yaml
================================================
services:
test:
build:
dockerfile_inline: FROM alpine
command: ping localhost
volumes:
- /data
develop:
watch:
- path: .
target: /data
initial_sync: true
action: sync+exec
exec:
command: echo "SUCCESS"
================================================
FILE: pkg/e2e/fixtures/watch/include.yaml
================================================
services:
a:
build:
dockerfile_inline: |
FROM nginx
RUN mkdir /data/
develop:
watch:
- path: .
include: A.*
target: /data/
action: sync
================================================
FILE: pkg/e2e/fixtures/watch/rebuild.yaml
================================================
services:
a:
build:
dockerfile_inline: |
FROM nginx
RUN mkdir /data
COPY test /data/a
develop:
watch:
- path: test
action: rebuild
b:
build:
dockerfile_inline: |
FROM nginx
RUN mkdir /data
COPY test /data/b
develop:
watch:
- path: test
action: rebuild
c:
build:
dockerfile_inline: |
FROM nginx
RUN mkdir /data
COPY test /data/c
develop:
watch:
- path: test
action: rebuild
================================================
FILE: pkg/e2e/fixtures/watch/with-external-network.yaml
================================================
services:
ext-alpine:
build:
dockerfile_inline: |-
FROM alpine
init: true
command: sleep infinity
develop:
watch:
- action: rebuild
path: .env
networks:
- external_network_test
networks:
external_network_test:
name: e2e-watch-external_network_test
external: true
================================================
FILE: pkg/e2e/fixtures/watch/x-initialSync.yaml
================================================
services:
test:
build:
dockerfile_inline: FROM alpine
command: ping localhost
volumes:
- /data
develop:
watch:
- path: .
target: /data
action: sync+exec
exec:
command: echo "SUCCESS"
x-initialSync: true
================================================
FILE: pkg/e2e/fixtures/wrong-composefile/build-error.yml
================================================
services:
simple:
build: service1
================================================
FILE: pkg/e2e/fixtures/wrong-composefile/compose.yaml
================================================
services:
simple:
image: nginx:alpine
wrongField: test
================================================
FILE: pkg/e2e/fixtures/wrong-composefile/service1/Dockerfile
================================================
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx
WRONG DOCKERFILE
================================================
FILE: pkg/e2e/fixtures/wrong-composefile/unknown-image.yml
================================================
services:
simple:
image: unknownimage
================================================
FILE: pkg/e2e/framework.go
================================================
/*
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 (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
cp "github.com/otiai10/copy"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
"github.com/docker/compose/v5/cmd/compose"
)
var (
// DockerExecutableName is the OS dependent Docker CLI binary name
DockerExecutableName = "docker"
// DockerComposeExecutableName is the OS dependent Docker CLI binary name
DockerComposeExecutableName = "docker-" + compose.PluginName
// DockerScanExecutableName is the OS dependent Docker Scan plugin binary name
DockerScanExecutableName = "docker-scan"
// DockerBuildxExecutableName is the Os dependent Buildx plugin binary name
DockerBuildxExecutableName = "docker-buildx"
// DockerModelExecutableName is the Os dependent Docker-Model plugin binary name
DockerModelExecutableName = "docker-model"
// WindowsExecutableSuffix is the Windows executable suffix
WindowsExecutableSuffix = ".exe"
)
func init() {
if runtime.GOOS == "windows" {
DockerExecutableName += WindowsExecutableSuffix
DockerComposeExecutableName += WindowsExecutableSuffix
DockerScanExecutableName += WindowsExecutableSuffix
DockerBuildxExecutableName += WindowsExecutableSuffix
}
}
// CLI is used to wrap the CLI for end to end testing
type CLI struct {
// ConfigDir for Docker configuration (set as DOCKER_CONFIG)
ConfigDir string
// HomeDir for tools that look for user files (set as HOME)
HomeDir string
// env overrides to apply to every invoked command
//
// To populate, use WithEnv when creating a CLI instance.
env []string
}
// CLIOption to customize behavior for all commands for a CLI instance.
type CLIOption func(c *CLI)
// NewParallelCLI marks the parent test as parallel and returns a CLI instance
// suitable for usage across child tests.
func NewParallelCLI(t *testing.T, opts ...CLIOption) *CLI {
t.Helper()
t.Parallel()
return NewCLI(t, opts...)
}
// NewCLI creates a CLI instance for running E2E tests.
func NewCLI(t testing.TB, opts ...CLIOption) *CLI {
t.Helper()
configDir := t.TempDir()
copyLocalConfig(t, configDir)
initializePlugins(t, configDir)
initializeContextDir(t, configDir)
c := &CLI{
ConfigDir: configDir,
HomeDir: t.TempDir(),
}
for _, opt := range opts {
opt(c)
}
c.RunDockerComposeCmdNoCheck(t, "version")
return c
}
// WithEnv sets environment variables that will be passed to commands.
func WithEnv(env ...string) CLIOption {
return func(c *CLI) {
c.env = append(c.env, env...)
}
}
func copyLocalConfig(t testing.TB, configDir string) {
t.Helper()
// copy local config.json if exists
localConfig := filepath.Join(os.Getenv("HOME"), ".docker", "config.json")
// if no config present just continue
if _, err := os.Stat(localConfig); err != nil {
// copy the local config.json to the test config dir
CopyFile(t, localConfig, filepath.Join(configDir, "config.json"))
}
}
// initializePlugins copies the necessary plugin files to the temporary config
// directory for the test.
func initializePlugins(t testing.TB, configDir string) {
t.Cleanup(func() {
if t.Failed() {
if conf, err := os.ReadFile(filepath.Join(configDir, "config.json")); err == nil {
t.Logf("Config: %s\n", string(conf))
}
t.Log("Contents of config dir:")
for _, p := range dirContents(configDir) {
t.Logf(" - %s", p)
}
}
})
require.NoError(t, os.MkdirAll(filepath.Join(configDir, "cli-plugins"), 0o755),
"Failed to create cli-plugins directory")
composePlugin, err := findExecutable(DockerComposeExecutableName)
if errors.Is(err, fs.ErrNotExist) {
t.Logf("WARNING: docker-compose cli-plugin not found")
}
if err == nil {
CopyFile(t, composePlugin, filepath.Join(configDir, "cli-plugins", DockerComposeExecutableName))
buildxPlugin, err := findPluginExecutable(DockerBuildxExecutableName)
if err != nil {
t.Logf("WARNING: docker-buildx cli-plugin not found, using default buildx installation.")
} else {
CopyFile(t, buildxPlugin, filepath.Join(configDir, "cli-plugins", DockerBuildxExecutableName))
}
// We don't need a functional scan plugin, but a valid plugin binary
CopyFile(t, composePlugin, filepath.Join(configDir, "cli-plugins", DockerScanExecutableName))
modelPlugin, err := findPluginExecutable(DockerModelExecutableName)
if err != nil {
t.Logf("WARNING: docker-model cli-plugin not found")
} else {
CopyFile(t, modelPlugin, filepath.Join(configDir, "cli-plugins", DockerModelExecutableName))
}
}
}
func initializeContextDir(t testing.TB, configDir string) {
dockerUserDir := ".docker/contexts"
userDir, err := os.UserHomeDir()
require.NoError(t, err, "Failed to get user home directory")
userContextsDir := filepath.Join(userDir, dockerUserDir)
if checkExists(userContextsDir) {
dstContexts := filepath.Join(configDir, "contexts")
require.NoError(t, cp.Copy(userContextsDir, dstContexts), "Failed to copy contexts directory")
}
}
func checkExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func dirContents(dir string) []string {
var res []string
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
res = append(res, path)
return nil
})
return res
}
func findExecutable(executableName string) (string, error) {
bin := os.Getenv("COMPOSE_E2E_BIN_PATH")
if bin == "" {
_, filename, _, _ := runtime.Caller(0)
buildPath := filepath.Join(filepath.Dir(filename), "..", "..", "bin", "build")
var err error
bin, err = filepath.Abs(filepath.Join(buildPath, executableName))
if err != nil {
return "", err
}
}
if _, err := os.Stat(bin); err == nil {
return bin, nil
}
return "", fmt.Errorf("looking for %q: %w", bin, fs.ErrNotExist)
}
func findPluginExecutable(pluginExecutableName string) (string, error) {
dockerUserDir := ".docker/cli-plugins"
userDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
candidates := []string{
filepath.Join(userDir, dockerUserDir),
"/usr/local/lib/docker/cli-plugins",
"/usr/local/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins",
"/usr/libexec/docker/cli-plugins",
}
for _, path := range candidates {
bin, err := filepath.Abs(filepath.Join(path, pluginExecutableName))
if err != nil {
return "", err
}
if _, err := os.Stat(bin); err == nil {
return bin, nil
}
}
return "", fmt.Errorf("plugin not found %s: %w", pluginExecutableName, os.ErrNotExist)
}
// CopyFile copies a file from a sourceFile to a destinationFile setting permissions to 0755
func CopyFile(t testing.TB, sourceFile string, destinationFile string) {
t.Helper()
src, err := os.Open(sourceFile)
require.NoError(t, err, "Failed to open source file: %s")
//nolint:errcheck
defer src.Close()
dst, err := os.OpenFile(destinationFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
require.NoError(t, err, "Failed to open destination file: %s", destinationFile)
//nolint:errcheck
defer dst.Close()
_, err = io.Copy(dst, src)
require.NoError(t, err, "Failed to copy file: %s", sourceFile)
}
// BaseEnvironment provides the minimal environment variables used across all
// Docker / Compose commands.
func (c *CLI) BaseEnvironment() []string {
env := []string{
"HOME=" + c.HomeDir,
"USER=" + os.Getenv("USER"),
"DOCKER_CONFIG=" + c.ConfigDir,
"KUBECONFIG=invalid",
"PATH=" + os.Getenv("PATH"),
}
dockerContextEnv, ok := os.LookupEnv("DOCKER_CONTEXT")
if ok {
env = append(env, "DOCKER_CONTEXT="+dockerContextEnv)
}
if coverdir, ok := os.LookupEnv("GOCOVERDIR"); ok {
_, filename, _, _ := runtime.Caller(0)
root := filepath.Join(filepath.Dir(filename), "..", "..")
coverdir = filepath.Join(root, coverdir)
env = append(env, fmt.Sprintf("GOCOVERDIR=%s", coverdir))
}
return env
}
// NewCmd creates a cmd object configured with the test environment set
func (c *CLI) NewCmd(command string, args ...string) icmd.Cmd {
return icmd.Cmd{
Command: append([]string{command}, args...),
Env: append(c.BaseEnvironment(), c.env...),
}
}
// NewCmdWithEnv creates a cmd object configured with the test environment set with additional env vars
func (c *CLI) NewCmdWithEnv(envvars []string, command string, args ...string) icmd.Cmd {
// base env -> CLI overrides -> cmd overrides
cmdEnv := append(c.BaseEnvironment(), c.env...)
cmdEnv = append(cmdEnv, envvars...)
return icmd.Cmd{
Command: append([]string{command}, args...),
Env: cmdEnv,
}
}
// MetricsSocket get the path where test metrics will be sent
func (c *CLI) MetricsSocket() string {
return filepath.Join(c.ConfigDir, "docker-cli.sock")
}
// NewDockerCmd creates a docker cmd without running it
func (c *CLI) NewDockerCmd(t testing.TB, args ...string) icmd.Cmd {
t.Helper()
for _, arg := range args {
if arg == compose.PluginName {
t.Fatal("This test called 'RunDockerCmd' for 'compose'. Please prefer 'RunDockerComposeCmd' to be able to test as a plugin and standalone")
}
}
return c.NewCmd(DockerExecutableName, args...)
}
// RunDockerOrExitError runs a docker command and returns a result
func (c *CLI) RunDockerOrExitError(t testing.TB, args ...string) *icmd.Result {
t.Helper()
t.Logf("\t[%s] docker %s\n", t.Name(), strings.Join(args, " "))
return icmd.RunCmd(c.NewDockerCmd(t, args...))
}
// RunCmd runs a command, expects no error and returns a result
func (c *CLI) RunCmd(t testing.TB, args ...string) *icmd.Result {
t.Helper()
t.Logf("\t[%s] %s\n", t.Name(), strings.Join(args, " "))
assert.Assert(t, len(args) >= 1, "require at least one command in parameters")
res := icmd.RunCmd(c.NewCmd(args[0], args[1:]...))
res.Assert(t, icmd.Success)
return res
}
// RunCmdInDir runs a command in a given dir, expects no error and returns a result
func (c *CLI) RunCmdInDir(t testing.TB, dir string, args ...string) *icmd.Result {
t.Helper()
t.Logf("\t[%s] %s\n", t.Name(), strings.Join(args, " "))
assert.Assert(t, len(args) >= 1, "require at least one command in parameters")
cmd := c.NewCmd(args[0], args[1:]...)
cmd.Dir = dir
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Success)
return res
}
// RunDockerCmd runs a docker command, expects no error and returns a result
func (c *CLI) RunDockerCmd(t testing.TB, args ...string) *icmd.Result {
t.Helper()
res := c.RunDockerOrExitError(t, args...)
res.Assert(t, icmd.Success)
return res
}
// RunDockerComposeCmd runs a docker compose command, expects no error and returns a result
func (c *CLI) RunDockerComposeCmd(t testing.TB, args ...string) *icmd.Result {
t.Helper()
res := c.RunDockerComposeCmdNoCheck(t, args...)
res.Assert(t, icmd.Success)
return res
}
// RunDockerComposeCmdNoCheck runs a docker compose command, don't presume of any expectation and returns a result
func (c *CLI) RunDockerComposeCmdNoCheck(t testing.TB, args ...string) *icmd.Result {
t.Helper()
cmd := c.NewDockerComposeCmd(t, args...)
cmd.Stdout = os.Stdout
t.Logf("Running command: %s", strings.Join(cmd.Command, " "))
return icmd.RunCmd(cmd)
}
// NewDockerComposeCmd creates a command object for Compose, either in plugin
// or standalone mode (based on build tags).
func (c *CLI) NewDockerComposeCmd(t testing.TB, args ...string) icmd.Cmd {
t.Helper()
if composeStandaloneMode {
return c.NewCmd(ComposeStandalonePath(t), args...)
}
args = append([]string{"compose"}, args...)
return c.NewCmd(DockerExecutableName, args...)
}
// ComposeStandalonePath returns the path to the locally-built Compose
// standalone binary from the repo.
//
// This function will fail the test immediately if invoked when not running
// in standalone test mode.
func ComposeStandalonePath(t testing.TB) string {
t.Helper()
if !composeStandaloneMode {
require.Fail(t, "Not running in standalone mode")
}
composeBinary, err := findExecutable(DockerComposeExecutableName)
require.NoError(t, err, "Could not find standalone Compose binary (%q)",
DockerComposeExecutableName)
return composeBinary
}
// StdoutContains returns a predicate on command result expecting a string in stdout
func StdoutContains(expected string) func(*icmd.Result) bool {
return func(res *icmd.Result) bool {
return strings.Contains(res.Stdout(), expected)
}
}
func IsHealthy(service string) func(res *icmd.Result) bool {
return func(res *icmd.Result) bool {
type state struct {
Name string `json:"name"`
Health string `json:"health"`
}
decoder := json.NewDecoder(strings.NewReader(res.Stdout()))
for decoder.More() {
ps := state{}
err := decoder.Decode(&ps)
if err != nil {
return false
}
if ps.Name == service && ps.Health == "healthy" {
return true
}
}
return false
}
}
// WaitForCmdResult try to execute a cmd until resulting output matches given predicate
func (c *CLI) WaitForCmdResult(
t testing.TB,
command icmd.Cmd,
predicate func(*icmd.Result) bool,
timeout time.Duration,
delay time.Duration,
) {
t.Helper()
assert.Assert(t, timeout.Nanoseconds() > delay.Nanoseconds(), "timeout must be greater than delay")
var res *icmd.Result
checkStopped := func(logt poll.LogT) poll.Result {
fmt.Printf("\t[%s] %s\n", t.Name(), strings.Join(command.Command, " "))
res = icmd.RunCmd(command)
if !predicate(res) {
return poll.Continue("Cmd output did not match requirement: %q", res.Combined())
}
return poll.Success()
}
poll.WaitOn(t, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
}
// WaitForCondition wait for predicate to execute to true
func (c *CLI) WaitForCondition(
t testing.TB,
predicate func() (bool, string),
timeout time.Duration,
delay time.Duration,
) {
t.Helper()
checkStopped := func(logt poll.LogT) poll.Result {
pass, description := predicate()
if !pass {
return poll.Continue("Condition not met: %q", description)
}
return poll.Success()
}
poll.WaitOn(t, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
}
// Lines split output into lines
func Lines(output string) []string {
return strings.Split(strings.TrimSpace(output), "\n")
}
// HTTPGetWithRetry performs an HTTP GET on an `endpoint`, using retryDelay also as a request timeout.
// In the case of an error or the response status is not the expected one, it retries the same request,
// returning the response body as a string (empty if we could not reach it)
func HTTPGetWithRetry(
t testing.TB,
endpoint string,
expectedStatus int,
retryDelay time.Duration,
timeout time.Duration,
) string {
t.Helper()
var (
r *http.Response
err error
)
client := &http.Client{
Timeout: retryDelay,
}
fmt.Printf("\t[%s] GET %s\n", t.Name(), endpoint)
checkUp := func(t poll.LogT) poll.Result {
r, err = client.Get(endpoint)
if err != nil {
return poll.Continue("reaching %q: Error %s", endpoint, err.Error())
}
if r.StatusCode == expectedStatus {
return poll.Success()
}
return poll.Continue("reaching %q: %d != %d", endpoint, r.StatusCode, expectedStatus)
}
poll.WaitOn(t, checkUp, poll.WithDelay(retryDelay), poll.WithTimeout(timeout))
if r != nil {
b, err := io.ReadAll(r.Body)
assert.NilError(t, err)
return string(b)
}
return ""
}
func (c *CLI) cleanupWithDown(t testing.TB, project string, args ...string) {
t.Helper()
c.RunDockerComposeCmd(t, append([]string{"-p", project, "down", "-v", "--remove-orphans"}, args...)...)
}
================================================
FILE: pkg/e2e/healthcheck_test.go
================================================
/*
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 (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestStartInterval(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-interval"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start_interval/compose.yaml", "--project-name", projectName, "up", "--wait", "-d", "error")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "healthcheck.start_interval requires healthcheck.start_period to be set"})
timeout := time.After(30 * time.Second)
done := make(chan bool)
go func() {
//nolint:nolintlint,testifylint // helper asserts inside goroutine; acceptable in this e2e test
res := c.RunDockerComposeCmd(t, "-f", "fixtures/start_interval/compose.yaml", "--project-name", projectName, "up", "--wait", "-d", "test")
out := res.Combined()
assert.Assert(t, strings.Contains(out, "Healthy"), out)
done <- true
}()
select {
case <-timeout:
t.Fatal("test did not finish in time")
case <-done:
break
}
}
================================================
FILE: pkg/e2e/hooks_test.go
================================================
/*
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 (
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestPostStartHookInError(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-post-start-failure"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/hooks/poststart/compose-error.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 1})
assert.Assert(t, strings.Contains(res.Combined(), "test hook exited with status 127"), res.Combined())
}
func TestPostStartHookSuccess(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-post-start-success"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/poststart/compose-success.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
}
func TestPreStopHookSuccess(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-pre-stop-success"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/prestop/compose-success.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/prestop/compose-success.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/prestop/compose-success.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
res.Assert(t, icmd.Expected{ExitCode: 0})
}
func TestPreStopHookInError(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-pre-stop-failure"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/prestop/compose-success.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/hooks/prestop/compose-error.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/hooks/prestop/compose-error.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
res.Assert(t, icmd.Expected{ExitCode: 1})
assert.Assert(t, strings.Contains(res.Combined(), "sample hook exited with status 127"))
}
func TestPreStopHookSuccessWithPreviousStop(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-pre-stop-success-with-previous-stop"
t.Cleanup(func() {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/compose.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
res.Assert(t, icmd.Expected{ExitCode: 0})
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/compose.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/compose.yaml", "--project-name", projectName, "stop", "sample")
res.Assert(t, icmd.Expected{ExitCode: 0})
}
func TestPostStartAndPreStopHook(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "hooks-post-start-and-pre-stop"
t.Cleanup(func() {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/compose.yaml", "--project-name", projectName, "down", "-v", "--remove-orphans", "-t", "0")
res.Assert(t, icmd.Expected{ExitCode: 0})
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/hooks/compose.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
}
================================================
FILE: pkg/e2e/ipc_test.go
================================================
/*
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 (
"fmt"
"strings"
"testing"
"gotest.tools/v3/icmd"
)
func TestIPC(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "ipc_e2e"
var cid string
t.Run("create ipc mode container", func(t *testing.T) {
res := c.RunDockerCmd(t, "run", "-d", "--rm", "--ipc=shareable", "--name", "ipc_mode_container", "alpine",
"top")
cid = strings.Trim(res.Stdout(), "\n")
})
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/ipc-test/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("check running project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `shareable`})
})
t.Run("check ipcmode in container inspect", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-shareable-1")
res.Assert(t, icmd.Expected{Out: `"IpcMode": "shareable",`})
res = c.RunDockerCmd(t, "inspect", projectName+"-service-1")
res.Assert(t, icmd.Expected{Out: `"IpcMode": "container:`})
res = c.RunDockerCmd(t, "inspect", projectName+"-container-1")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`"IpcMode": "container:%s",`, cid)})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("remove ipc mode container", func(t *testing.T) {
_ = c.RunDockerCmd(t, "rm", "-f", "ipc_mode_container")
})
}
================================================
FILE: pkg/e2e/logs_test.go
================================================
/*
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 (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestLocalComposeLogs(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-logs"
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("logs", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs")
res.Assert(t, icmd.Expected{Out: `PING localhost`})
res.Assert(t, icmd.Expected{Out: `hello`})
})
t.Run("logs ping", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "ping")
res.Assert(t, icmd.Expected{Out: `PING localhost`})
assert.Assert(t, !strings.Contains(res.Stdout(), "hello"))
})
t.Run("logs hello", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "hello", "ping")
res.Assert(t, icmd.Expected{Out: `PING localhost`})
res.Assert(t, icmd.Expected{Out: `hello`})
})
t.Run("logs hello index", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "--index", "2", "hello")
// docker-compose logs hello
// logs-test-hello-2 | hello
// logs-test-hello-1 | hello
t.Log(res.Stdout())
assert.Assert(t, !strings.Contains(res.Stdout(), "hello-1"))
assert.Assert(t, strings.Contains(res.Stdout(), "hello-2"))
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestLocalComposeLogsFollow(t *testing.T) {
c := NewCLI(t, WithEnv("REPEAT=20"))
const projectName = "compose-e2e-logs"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "ping")
cmd := c.NewDockerComposeCmd(t, "--project-name", projectName, "logs", "-f")
res := icmd.StartCmd(cmd)
t.Cleanup(func() {
_ = res.Cmd.Process.Kill()
})
poll.WaitOn(t, expectOutput(res, "ping-1 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
poll.WaitOn(t, expectOutput(res, "hello-1 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "--scale", "ping=2", "ping")
poll.WaitOn(t, expectOutput(res, "ping-2 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second))
}
func TestLocalComposeLargeLogs(t *testing.T) {
const projectName = "compose-e2e-large_logs"
file := filepath.Join(t.TempDir(), "large.txt")
c := NewCLI(t, WithEnv("FILE="+file))
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
f, err := os.Create(file)
assert.NilError(t, err)
for i := range 300_000 {
_, err := io.WriteString(f, fmt.Sprintf("This is line %d in a laaaarge text file\n", i))
assert.NilError(t, err)
}
assert.NilError(t, f.Close())
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/logs-test/cat.yaml", "--project-name", projectName, "up", "--abort-on-container-exit", "--menu=false")
cmd.Stdout = io.Discard
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{Out: "test-1 exited with code 0"})
}
func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result {
return func(t poll.LogT) poll.Result {
if strings.Contains(res.Stdout(), expected) {
return poll.Success()
}
return poll.Continue("condition not met")
}
}
================================================
FILE: pkg/e2e/main_test.go
================================================
/*
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 (
"os"
"testing"
)
func TestMain(m *testing.M) {
exitCode := m.Run()
os.Exit(exitCode)
}
================================================
FILE: pkg/e2e/model_test.go
================================================
/*
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 (
"testing"
)
func TestComposeModel(t *testing.T) {
t.Skip("waiting for docker-model release")
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "model-test")
c.RunDockerComposeCmd(t, "-f", "./fixtures/model/compose.yaml", "run", "test", "sh", "-c", "curl ${FOO_URL}")
}
================================================
FILE: pkg/e2e/networks_test.go
================================================
/*
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 (
"fmt"
"net/http"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestNetworks(t *testing.T) {
// fixture is shared with TestNetworkModes and is not safe to run concurrently
const projectName = "network-e2e"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName,
"COMPOSE_FILE=./fixtures/network-test/compose.yaml",
))
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
c.RunDockerComposeCmd(t, "up", "-d")
res := c.RunDockerComposeCmd(t, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:80"
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
res = c.RunDockerCmd(t, "network", "ls")
res.Assert(t, icmd.Expected{Out: projectName + "_dbnet"})
res.Assert(t, icmd.Expected{Out: "microservices"})
res = c.RunDockerComposeCmd(t, "port", "words", "8080")
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
res = c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "microservices"), res.Combined())
}
func TestNetworkAliases(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "network_alias_e2e"
defer c.cleanupWithDown(t, projectName)
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "up",
"-d")
})
t.Run("curl alias", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName,
"exec", "-T", "container1", "curl", "http://alias-of-container2/")
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
})
t.Run("curl links", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName,
"exec", "-T", "container1", "curl", "http://container/")
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestNetworkLinks(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "network_link_e2e"
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-links/compose.yaml", "--project-name", projectName, "up",
"-d")
})
t.Run("curl links in default bridge network", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-links/compose.yaml", "--project-name", projectName,
"exec", "-T", "container2", "curl", "http://container1/")
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestIPAMConfig(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "ipam_e2e"
t.Run("ensure we do not reuse previous networks", func(t *testing.T) {
c.RunDockerOrExitError(t, "network", "rm", projectName+"_default")
})
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/ipam/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("ensure service get fixed IP assigned", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-foo-1", "-f",
fmt.Sprintf(`{{ $network := index .NetworkSettings.Networks "%s_default" }}{{ $network.IPAMConfig.IPv4Address }}`, projectName))
res.Assert(t, icmd.Expected{Out: "10.1.0.100"})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestNetworkModes(t *testing.T) {
// fixture is shared with TestNetworks and is not safe to run concurrently
c := NewCLI(t)
const projectName = "network_mode_service_run"
defer c.cleanupWithDown(t, projectName)
t.Run("run with service mode dependency", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "run", "-T", "mydb", "echo", "success")
res.Assert(t, icmd.Expected{Out: "success"})
})
}
func TestNetworkConfigChanged(t *testing.T) {
t.Skip("unstable")
// fixture is shared with TestNetworks and is not safe to run concurrently
c := NewCLI(t)
const projectName = "network_config_change"
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-test/compose.subnet.yaml", "--project-name", projectName, "up", "-d")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "hostname", "-i")
res.Assert(t, icmd.Expected{Out: "172.99.0."})
res.Combined()
cmd := c.NewCmdWithEnv([]string{"SUBNET=192.168.0.0/16"},
"docker", "compose", "-f", "./fixtures/network-test/compose.subnet.yaml", "--project-name", projectName, "up", "-d")
res = icmd.RunCmd(cmd)
res.Assert(t, icmd.Success)
out := res.Combined()
fmt.Println(out)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "hostname", "-i")
res.Assert(t, icmd.Expected{Out: "192.168.0."})
}
func TestMacAddress(t *testing.T) {
c := NewCLI(t)
const projectName = "network_mac_address"
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-test/mac_address.yaml", "--project-name", projectName, "up", "-d")
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-test-1", projectName), "-f", "{{ (index .NetworkSettings.Networks \"network_mac_address_default\" ).MacAddress }}")
res.Assert(t, icmd.Expected{Out: "00:e0:84:35:d0:e8"})
}
func TestInterfaceName(t *testing.T) {
c := NewCLI(t)
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
major, _, found := strings.Cut(version.Combined(), ".")
assert.Assert(t, found)
if major == "26" || major == "27" {
t.Skip("Skipping test due to docker version < 28")
}
const projectName = "network_interface_name"
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-interface-name/compose.yaml", "--project-name", projectName, "run", "test")
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
res.Assert(t, icmd.Expected{Out: "foobar@"})
}
func TestNetworkRecreate(t *testing.T) {
c := NewCLI(t)
const projectName = "network_recreate"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml", "--project-name", projectName, "up", "-d")
c = NewCLI(t, WithEnv("FOO=bar"))
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml", "--project-name", projectName, "--progress=plain", "up", "-d")
err := res.Stderr()
fmt.Println(err)
hasStopped := strings.Contains(err, "Stopped")
hasResumed := strings.Contains(err, "Started") || strings.Contains(err, "Recreated")
if !hasStopped || !hasResumed {
t.Fatalf("unexpected output, missing expected events, stderr: %s", err)
}
}
================================================
FILE: pkg/e2e/noDeps_test.go
================================================
//go:build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"fmt"
"testing"
"gotest.tools/v3/icmd"
)
func TestNoDepsVolumeFrom(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-no-deps-volume-from"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
c.RunDockerCmd(t, "rm", "-f", fmt.Sprintf("%s-db-1", projectName))
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot share volume with service db: container missing"})
}
func TestNoDepsNetworkMode(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-no-deps-network-mode"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/network-mode.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/network-mode.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
c.RunDockerCmd(t, "rm", "-f", fmt.Sprintf("%s-db-1", projectName))
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/no-deps/network-mode.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot share network namespace with service db: container missing"})
}
================================================
FILE: pkg/e2e/orphans_test.go
================================================
/*
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 TestRemoveOrphans(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-orphans"
defer c.cleanupWithDown(t, projectName)
c.RunDockerComposeCmd(t, "-f", "./fixtures/orphans/compose.yaml", "-p", projectName, "run", "orphan")
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--all")
assert.Check(t, strings.Contains(res.Combined(), "compose-e2e-orphans-orphan-run-"))
c.RunDockerComposeCmd(t, "-f", "./fixtures/orphans/compose.yaml", "-p", projectName, "up", "-d")
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--all")
assert.Check(t, !strings.Contains(res.Combined(), "compose-e2e-orphans-orphan-run-"))
}
================================================
FILE: pkg/e2e/pause_test.go
================================================
/*
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 (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"gotest.tools/v3/icmd"
)
func TestPause(t *testing.T) {
if _, ok := os.LookupEnv("CI"); ok {
t.Skip("Skipping test on CI... flaky")
}
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-pause",
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))
cleanup := func() {
cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0")
}
cleanup()
t.Cleanup(cleanup)
// launch both services and verify that they are accessible
cli.RunDockerComposeCmd(t, "up", "-d")
urls := map[string]string{
"a": urlForService(t, cli, "a", 80),
"b": urlForService(t, cli, "b", 80),
}
for _, url := range urls {
HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 20*time.Second)
}
// pause a and verify that it can no longer be hit but b still can
cli.RunDockerComposeCmd(t, "pause", "a")
httpClient := http.Client{Timeout: 250 * time.Millisecond}
resp, err := httpClient.Get(urls["a"])
if resp != nil {
_ = resp.Body.Close()
}
require.Error(t, err, "a should no longer respond")
var netErr net.Error
errors.As(err, &netErr)
require.True(t, netErr.Timeout(), "Error should have indicated a timeout")
HTTPGetWithRetry(t, urls["b"], http.StatusOK, 50*time.Millisecond, 5*time.Second)
// unpause a and verify that both containers work again
cli.RunDockerComposeCmd(t, "unpause", "a")
for _, url := range urls {
HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 5*time.Second)
}
}
func TestPauseServiceNotRunning(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-pause-svc-not-running",
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))
cleanup := func() {
cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0")
}
cleanup()
t.Cleanup(cleanup)
// pause a and verify that it can no longer be hit but b still can
res := cli.RunDockerComposeCmdNoCheck(t, "pause", "a")
// TODO: `docker pause` errors in this case, should Compose be consistent?
res.Assert(t, icmd.Expected{ExitCode: 0})
}
func TestPauseServiceAlreadyPaused(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-pause-svc-already-paused",
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))
cleanup := func() {
cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0")
}
cleanup()
t.Cleanup(cleanup)
// launch a and wait for it to come up
cli.RunDockerComposeCmd(t, "up", "--menu=false", "--wait", "a")
HTTPGetWithRetry(t, urlForService(t, cli, "a", 80), http.StatusOK, 50*time.Millisecond, 10*time.Second)
// pause a twice - first time should pass, second time fail
cli.RunDockerComposeCmd(t, "pause", "a")
res := cli.RunDockerComposeCmdNoCheck(t, "pause", "a")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "already paused"})
}
func TestPauseServiceDoesNotExist(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-pause-svc-not-exist",
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))
cleanup := func() {
cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0")
}
cleanup()
t.Cleanup(cleanup)
// pause a and verify that it can no longer be hit but b still can
res := cli.RunDockerComposeCmdNoCheck(t, "pause", "does_not_exist")
// TODO: `compose down does_not_exist` and similar error, this should too
res.Assert(t, icmd.Expected{ExitCode: 0})
}
func urlForService(t testing.TB, cli *CLI, service string, targetPort int) string {
t.Helper()
return fmt.Sprintf(
"http://localhost:%d",
publishedPortForService(t, cli, service, targetPort),
)
}
func publishedPortForService(t testing.TB, cli *CLI, service string, targetPort int) int {
t.Helper()
res := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
var svc struct {
Publishers []struct {
TargetPort int
PublishedPort int
}
}
require.NoError(t, json.Unmarshal([]byte(res.Stdout()), &svc),
"Failed to parse `%s` output", res.Cmd.String())
for _, pp := range svc.Publishers {
if pp.TargetPort == targetPort {
return pp.PublishedPort
}
}
require.Failf(t, "No published port for target port",
"Target port: %d\nService: %s", targetPort, res.Combined())
return -1
}
================================================
FILE: pkg/e2e/profiles_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
const (
profiledService = "profiled-service"
regularService = "regular-service"
)
func TestExplicitProfileUsage(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-explicit-profiles"
const profileName = "test-profile"
t.Run("compose up with profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "--profile", profileName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: regularService})
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("compose stop with profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "--profile", profileName, "stop")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("compose start with profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "--profile", profileName, "start")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
res.Assert(t, icmd.Expected{Out: regularService})
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("compose restart with profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "--profile", profileName, "restart")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
res.Assert(t, icmd.Expected{Out: regularService})
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("check containers after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
})
}
func TestNoProfileUsage(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-no-profiles"
t.Run("compose up without profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: regularService})
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("compose stop without profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "stop")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("compose start without profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "start")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
res.Assert(t, icmd.Expected{Out: regularService})
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("compose restart without profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "restart")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
res.Assert(t, icmd.Expected{Out: regularService})
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("check containers after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
})
}
func TestActiveProfileViaTargetedService(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-via-target-service-profiles"
const profileName = "test-profile"
t.Run("compose up with service name", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "up", profiledService, "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
res.Assert(t, icmd.Expected{Out: profiledService})
res = c.RunDockerComposeCmd(t, "-p", projectName, "--profile", profileName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("compose stop with service name", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "stop", profiledService)
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
assert.Assert(t, !strings.Contains(res.Combined(), profiledService))
})
t.Run("compose start with service name", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "start", profiledService)
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("compose restart with service name", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"-p", projectName, "restart")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), regularService))
res.Assert(t, icmd.Expected{Out: profiledService})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("check containers after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
})
}
func TestDotEnvProfileUsage(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-dotenv-profiles"
const profileName = "test-profile"
t.Cleanup(func() {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("compose up with profile", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/compose.yaml",
"--env-file", "./fixtures/profiles/test-profile.env",
"-p", projectName, "--profile", profileName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 0})
res = c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: regularService})
res.Assert(t, icmd.Expected{Out: profiledService})
})
}
================================================
FILE: pkg/e2e/providers_test.go
================================================
/*
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 (
"bufio"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestDependsOnMultipleProviders(t *testing.T) {
provider, err := findExecutable("example-provider")
assert.NilError(t, err)
path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider))
c := NewParallelCLI(t, WithEnv("PATH="+path))
const projectName = "depends-on-multiple-providers"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/providers/depends-on-multiple-providers.yaml", "--project-name", projectName, "up")
res.Assert(t, icmd.Success)
env := getEnv(res.Combined(), false)
assert.Check(t, slices.Contains(env, "PROVIDER1_URL=https://magic.cloud/provider1"), env)
assert.Check(t, slices.Contains(env, "PROVIDER2_URL=https://magic.cloud/provider2"), env)
}
func getEnv(out string, run bool) []string {
var env []string
scanner := bufio.NewScanner(strings.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
if !run && strings.HasPrefix(line, "test-1 | ") {
env = append(env, line[10:])
}
if run && strings.Contains(line, "=") && len(strings.Split(line, "=")) == 2 {
env = append(env, line)
}
}
slices.Sort(env)
return env
}
================================================
FILE: pkg/e2e/ps_test.go
================================================
/*
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 (
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/icmd"
"github.com/docker/compose/v5/pkg/api"
)
func TestPs(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-ps"
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "up", "-d")
require.NoError(t, res.Error)
t.Cleanup(func() {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined())
t.Run("table", func(t *testing.T) {
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps")
lines := strings.Split(res.Stdout(), "\n")
assert.Len(t, lines, 4)
count := 0
for _, line := range lines[1:3] {
if strings.Contains(line, "e2e-ps-busybox-1") {
assert.Contains(t, line, "127.0.0.1:8001->8000/tcp")
count++
}
if strings.Contains(line, "e2e-ps-nginx-1") {
assert.Contains(t, line, "80/tcp, 443/tcp, 8080/tcp")
count++
}
}
assert.Equal(t, 2, count, "Did not match both services:\n"+res.Combined())
})
t.Run("json", func(t *testing.T) {
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps",
"--format", "json")
type element struct {
Name string
Project string
Publishers api.PortPublishers
}
var output []element
out := res.Stdout()
dec := json.NewDecoder(strings.NewReader(out))
for dec.More() {
var s element
require.NoError(t, dec.Decode(&s), "Failed to unmarshal ps JSON output")
output = append(output, s)
}
count := 0
assert.Len(t, output, 2)
for _, service := range output {
assert.Equal(t, projectName, service.Project)
publishers := service.Publishers
if service.Name == "e2e-ps-busybox-1" {
assert.Len(t, publishers, 1)
assert.Equal(t, api.PortPublishers{
{
URL: "127.0.0.1",
TargetPort: 8000,
PublishedPort: 8001,
Protocol: "tcp",
},
}, publishers)
count++
}
if service.Name == "e2e-ps-nginx-1" {
assert.Len(t, publishers, 3)
assert.Equal(t, api.PortPublishers{
{TargetPort: 80, Protocol: "tcp"},
{TargetPort: 443, Protocol: "tcp"},
{TargetPort: 8080, Protocol: "tcp"},
}, publishers)
count++
}
}
assert.Equal(t, 2, count, "Did not match both services:\n"+res.Combined())
})
t.Run("ps --all", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
require.NoError(t, res.Error)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps")
lines := strings.Split(res.Stdout(), "\n")
assert.Len(t, lines, 2)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps", "--all")
lines = strings.Split(res.Stdout(), "\n")
assert.Len(t, lines, 4)
})
t.Run("ps unknown", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
require.NoError(t, res.Error)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps", "nginx")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps", "unknown")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "no such service: unknown"})
})
}
================================================
FILE: pkg/e2e/publish_test.go
================================================
/*
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 (
"fmt"
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestPublishChecks(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-explicit-profiles"
t.Run("publish error env_file", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-env-file.yml",
"-p", projectName, "publish", "test/test")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `service "serviceA" has env_file declared.
To avoid leaking sensitive data,`})
})
t.Run("publish multiple errors env_file and environment", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-multi-env-config.yml",
"-p", projectName, "publish", "test/test")
// we don't in which order the services will be loaded, so we can't predict the order of the error messages
assert.Assert(t, strings.Contains(res.Combined(), `service "serviceB" has env_file declared.`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,
or remove sensitive data from your Compose configuration
`), res.Combined())
})
t.Run("publish success environment", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-environment.yml",
"-p", projectName, "publish", "test/test", "--with-env", "-y", "--dry-run")
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("publish success env_file", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-env-file.yml",
"-p", projectName, "publish", "test/test", "--with-env", "-y", "--dry-run")
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("publish with extends", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-with-extends.yml",
"-p", projectName, "publish", "test/test", "--dry-run")
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("refuse to publish with bind mount", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-bind-mount.yml",
"-p", projectName, "publish", "test/test", "--dry-run")
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
out := res.Combined()
assert.Assert(t, strings.Contains(out, "you are about to publish bind mounts declaration within your OCI artifact."), out)
assert.Assert(t, strings.Contains(out, "e2e/fixtures/publish:/user-data"), out)
assert.Assert(t, strings.Contains(out, "Are you ok to publish these bind mount declarations?"), out)
assert.Assert(t, !strings.Contains(out, "serviceA published"), out)
})
t.Run("publish with bind mount", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-bind-mount.yml",
"-p", projectName, "publish", "test/test", "--dry-run")
cmd.Stdin = strings.NewReader("y\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), "you are about to publish bind mounts declaration within your OCI artifact."), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations?"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e/fixtures/publish:/user-data"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("refuse to publish with build section only", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-build-only.yml",
"-p", projectName, "publish", "test/test", "--with-env", "-y", "--dry-run")
res.Assert(t, icmd.Expected{ExitCode: 1})
assert.Assert(t, strings.Contains(res.Combined(), "your Compose stack cannot be published as it only contains a build section for service(s):"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "serviceA"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "serviceB"), res.Combined())
})
t.Run("refuse to publish with local include", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-local-include.yml",
"-p", projectName, "publish", "test/test", "--dry-run")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot publish compose file with local includes"})
})
t.Run("detect sensitive data", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-sensitive.yml",
"-p", projectName, "publish", "test/test", "--with-env", "--dry-run")
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
output := res.Combined()
assert.Assert(t, strings.Contains(output, "you are about to publish sensitive data within your OCI artifact.\n"), output)
assert.Assert(t, strings.Contains(output, "please double check that you are not leaking sensitive data"), output)
assert.Assert(t, strings.Contains(output, "AWS Client ID\n\"services.serviceA.environment.AWS_ACCESS_KEY_ID\": A3TX1234567890ABCDEF"), output)
assert.Assert(t, strings.Contains(output, "AWS Secret Key\n\"services.serviceA.environment.AWS_SECRET_ACCESS_KEY\": aws\"12345+67890/abcdefghijklm+NOPQRSTUVWXYZ+\""), output)
assert.Assert(t, strings.Contains(output, "Github authentication\n\"GITHUB_TOKEN\": ghp_1234567890abcdefghijklmnopqrstuvwxyz"), output)
assert.Assert(t, strings.Contains(output, "JSON Web Token\n\"\": eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."+
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw"), output)
assert.Assert(t, strings.Contains(output, "Private Key\n\"\": -----BEGIN DSA PRIVATE KEY-----\nwxyz+ABC=\n-----END DSA PRIVATE KEY-----"), output)
})
}
func TestPublish(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-publish"
const registryName = projectName + "-registry"
c.RunDockerCmd(t, "run", "--name", registryName, "-P", "-d", "registry:3")
port := c.RunDockerCmd(t, "inspect", "--format", `{{ (index (index .NetworkSettings.Ports "5000/tcp") 0).HostPort }}`, registryName).Stdout()
registry := "localhost:" + strings.TrimSpace(port)
t.Cleanup(func() {
c.RunDockerCmd(t, "rm", "--force", registryName)
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/oci/compose.yaml", "-f", "./fixtures/publish/oci/compose-override.yaml",
"-p", projectName, "publish", "--with-env", "--yes", "--insecure-registry", registry+"/test:test")
res.Assert(t, icmd.Expected{ExitCode: 0})
// docker exec -it compose-e2e-publish-registry tree /var/lib/registry/docker/registry/v2/
cmd := c.NewDockerComposeCmd(t, "--verbose", "--project-name=oci",
"--insecure-registry", registry,
"-f", fmt.Sprintf("oci://%s/test:test", registry), "config")
res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "XDG_CACHE_HOME="+t.TempDir())
})
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Equal(t, res.Stdout(), `name: oci
services:
app:
environment:
HELLO: WORLD
image: alpine
networks:
default: null
networks:
default:
name: oci_default
`)
}
================================================
FILE: pkg/e2e/pull_test.go
================================================
/*
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"
"gotest.tools/v3/icmd"
)
func TestComposePull(t *testing.T) {
c := NewParallelCLI(t)
t.Run("Verify image pulled", func(t *testing.T) {
// cleanup existing images
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "down", "--rmi", "all")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
// verify default policy is 'always' for pull command
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull")
output = res.Combined()
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
})
t.Run("Verify skipped pull if image is already present locally", func(t *testing.T) {
// make sure the required image is present
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/image-present-locally", "pull")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/image-present-locally", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "alpine:3.13.12 Skipped Image is already present locally"))
// image with :latest tag gets pulled regardless if pull_policy: missing or if_not_present
assert.Assert(t, strings.Contains(output, "alpine:latest Pulled"))
})
t.Run("Verify skipped no image to be pulled", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/no-image-name-given", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "Skipped No image to be pulled"))
})
t.Run("Verify pull failure", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/compose-pull/unknown-image", "pull")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "pull access denied for does_not_exists"})
})
t.Run("Verify ignore pull failure", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/unknown-image", "pull", "--ignore-pull-failures")
res.Assert(t, icmd.Expected{Err: "Some service image(s) must be built from source by running:"})
})
}
================================================
FILE: pkg/e2e/recreate_no_deps_test.go
================================================
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"testing"
"gotest.tools/v3/icmd"
)
func TestRecreateWithNoDeps(t *testing.T) {
c := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=recreate-no-deps",
))
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/dependencies/recreate-no-deps.yaml", "up", "-d")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/dependencies/recreate-no-deps.yaml", "up", "-d", "--force-recreate", "--no-deps", "my-service")
res.Assert(t, icmd.Success)
RequireServiceState(t, c, "my-service", "running")
c.RunDockerComposeCmd(t, "down")
}
================================================
FILE: pkg/e2e/restart_test.go
================================================
/*
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 (
"fmt"
"strings"
"testing"
"time"
testify "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"
)
func assertServiceStatus(t *testing.T, projectName, service, status string, ps string) {
// match output with random spaces like:
// e2e-start-stop-db-1 alpine:latest "echo hello" db 1 minutes ago Exited (0) 1 minutes ago
regx := fmt.Sprintf("%s-%s-1.+%s\\s+.+%s.+", projectName, service, service, status)
testify.Regexp(t, regx, ps)
}
func TestRestart(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-restart"
t.Run("Up a project", func(t *testing.T) {
// This is just to ensure the containers do NOT exist
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--project-name", projectName, "up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-restart-1 Started"), res.Combined())
c.WaitForCmdResult(t, c.NewDockerComposeCmd(t, "--project-name", projectName, "ps", "-a", "--format",
"json"),
StdoutContains(`"State":"exited"`), 10*time.Second, 1*time.Second)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "-a")
assertServiceStatus(t, projectName, "restart", "Exited", res.Stdout())
c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--project-name", projectName, "restart")
// Give the same time but it must NOT exit
time.Sleep(time.Second)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assertServiceStatus(t, projectName, "restart", "Up", res.Stdout())
// Clean up
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestRestartWithDependencies(t *testing.T) {
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-restart-deps",
))
baseService := "nginx"
depWithRestart := "with-restart"
depNoRestart := "no-restart"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "down", "--remove-orphans")
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
res := c.RunDockerComposeCmd(t, "restart", baseService)
out := res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, !strings.Contains(out, depNoRestart), out)
c = NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-restart-deps",
"LABEL=recreate",
))
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
out = res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out)
}
func TestRestartWithProfiles(t *testing.T) {
c := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-restart-profiles",
))
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "down", "--remove-orphans")
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--profile", "test", "up", "-d")
res := c.RunDockerComposeCmd(t, "restart", "test")
fmt.Println(res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-profiles-test-1 Started"), res.Combined())
}
================================================
FILE: pkg/e2e/scale_test.go
================================================
/*
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 (
"fmt"
"strings"
"testing"
testify "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
const NO_STATE_TO_CHECK = ""
func TestScaleBasicCases(t *testing.T) {
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=scale-basic-tests"))
reset := func() {
c.RunDockerComposeCmd(t, "down", "--rmi", "all")
}
t.Cleanup(reset)
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d")
res.Assert(t, icmd.Success)
t.Log("scale up one service")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=2")
out := res.Combined()
checkServiceContainer(t, out, "scale-basic-tests-dbadmin", "Started", 2)
t.Log("scale up 2 services")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "front=3", "back=2")
out = res.Combined()
checkServiceContainer(t, out, "scale-basic-tests-front", "Running", 2)
checkServiceContainer(t, out, "scale-basic-tests-front", "Started", 1)
checkServiceContainer(t, out, "scale-basic-tests-back", "Running", 1)
checkServiceContainer(t, out, "scale-basic-tests-back", "Started", 1)
t.Log("scale down one service")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=1")
out = res.Combined()
checkServiceContainer(t, out, "scale-basic-tests-dbadmin", "Running", 1)
t.Log("scale to 0 a service")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=0")
assert.Check(t, res.Stdout() == "", res.Stdout())
t.Log("scale down 2 services")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "front=2", "back=1")
out = res.Combined()
checkServiceContainer(t, out, "scale-basic-tests-front", "Running", 2)
assert.Check(t, !strings.Contains(out, "Container scale-basic-tests-front-3 Running"), res.Combined())
checkServiceContainer(t, out, "scale-basic-tests-back", "Running", 1)
}
func TestScaleWithDepsCases(t *testing.T) {
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=scale-deps-tests"))
reset := func() {
c.RunDockerComposeCmd(t, "down", "--rmi", "all")
}
t.Cleanup(reset)
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps")
checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 2)
t.Log("scale up 1 service with --no-deps")
_ = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "--no-deps", "back=2")
res = c.RunDockerComposeCmd(t, "ps")
checkServiceContainer(t, res.Combined(), "scale-deps-tests-back", NO_STATE_TO_CHECK, 2)
checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 2)
t.Log("scale up 1 service without --no-deps")
_ = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "back=2")
res = c.RunDockerComposeCmd(t, "ps")
checkServiceContainer(t, res.Combined(), "scale-deps-tests-back", NO_STATE_TO_CHECK, 2)
checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 1)
}
func TestScaleUpAndDownPreserveContainerNumber(t *testing.T) {
const projectName = "scale-up-down-test"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName))
reset := func() {
c.RunDockerComposeCmd(t, "down", "--rmi", "all")
}
t.Cleanup(reset)
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2")
t.Log("scale down removes replica #2")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1")
t.Log("scale up restores replica #2")
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2")
}
func TestScaleDownRemovesObsolete(t *testing.T) {
const projectName = "scale-down-obsolete-test"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName))
reset := func() {
c.RunDockerComposeCmd(t, "down", "--rmi", "all")
}
t.Cleanup(reset)
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "db")
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1")
cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db")
res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "MAYBE=value")
})
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2")
t.Log("scale down removes obsolete replica #1")
cmd = c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db")
res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "MAYBE=value")
})
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db")
res.Assert(t, icmd.Success)
assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1")
}
func checkServiceContainer(t *testing.T, stdout, containerName, containerState string, count int) {
found := 0
lines := strings.SplitSeq(stdout, "\n")
for line := range lines {
if strings.Contains(line, containerName) && strings.Contains(line, containerState) {
found++
}
}
if found == count {
return
}
errMessage := fmt.Sprintf("expected %d but found %d instance(s) of container %s in stoud", count, found, containerName)
if containerState != "" {
errMessage += fmt.Sprintf(" with expected state %s", containerState)
}
testify.Fail(t, errMessage, stdout)
}
func TestScaleDownNoRecreate(t *testing.T) {
const projectName = "scale-down-recreated-test"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName))
reset := func() {
c.RunDockerComposeCmd(t, "down", "--rmi", "all")
}
t.Cleanup(reset)
c.RunDockerComposeCmd(t, "-f", "fixtures/scale/build.yaml", "build", "--build-arg", "FOO=test")
c.RunDockerComposeCmd(t, "-f", "fixtures/scale/build.yaml", "up", "-d", "--scale", "test=2")
c.RunDockerComposeCmd(t, "-f", "fixtures/scale/build.yaml", "build", "--build-arg", "FOO=updated")
c.RunDockerComposeCmd(t, "-f", "fixtures/scale/build.yaml", "up", "-d", "--scale", "test=4", "--no-recreate")
res := c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "test")
res.Assert(t, icmd.Success)
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-1"))
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-2"))
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-3"))
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-4"))
t.Log("scale down removes obsolete replica #1 and #2")
c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "test=2")
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "test")
res.Assert(t, icmd.Success)
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-3"))
assert.Check(t, strings.Contains(res.Stdout(), "scale-down-recreated-test-test-4"))
}
================================================
FILE: pkg/e2e/secrets_test.go
================================================
/*
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 (
"testing"
"gotest.tools/v3/icmd"
)
func TestSecretFromEnv(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "env-secret")
t.Run("compose run", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "foo"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "SECRET=BAR")
})
res.Assert(t, icmd.Expected{Out: "BAR"})
})
t.Run("secret uid", func(t *testing.T) {
res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "foo", "ls", "-al", "/var/run/secrets/bar"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "SECRET=BAR")
})
res.Assert(t, icmd.Expected{Out: "-r--r----- 1 1005 1005"})
})
}
func TestSecretFromInclude(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "env-secret-include")
t.Run("compose run", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "included")
res.Assert(t, icmd.Expected{Out: "this-is-secret"})
})
}
================================================
FILE: pkg/e2e/start_stop_test.go
================================================
/*
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 (
"fmt"
"strings"
"testing"
testify "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestStartStop(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-stop-no-dependencies"
getProjectRegx := func(status string) string {
// match output with random spaces like:
// e2e-start-stop running(3)
return fmt.Sprintf("%s\\s+%s\\(%d\\)", projectName, status, 2)
}
t.Run("Up a project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up",
"-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined())
res = c.RunDockerComposeCmd(t, "ls", "--all")
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assertServiceStatus(t, projectName, "simple", "Up", res.Stdout())
assertServiceStatus(t, projectName, "another", "Up", res.Stdout())
})
t.Run("stop project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "stop")
res := c.RunDockerComposeCmd(t, "ls")
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-no-dependencies"), res.Combined())
res = c.RunDockerComposeCmd(t, "ls", "--all")
testify.Regexp(t, getProjectRegx("exited"), res.Stdout())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-no-dependencies-words-1"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--all")
assertServiceStatus(t, projectName, "simple", "Exited", res.Stdout())
assertServiceStatus(t, projectName, "another", "Exited", res.Stdout())
})
t.Run("start project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "start")
res := c.RunDockerComposeCmd(t, "ls")
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestStartStopWithDependencies(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-stop-with-dependencies"
defer c.RunDockerComposeCmd(t, "--project-name", projectName, "rm", "-fsv")
t.Run("Up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName,
"up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("stop foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-bar-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-foo-1"), res.Combined())
})
t.Run("start foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "start", "foo")
out := res.Combined()
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out)
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
out = res.Combined()
assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-bar-1"), out)
assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-foo-1"), out)
})
t.Run("Up no-deps links", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/links/compose.yaml", "--project-name", projectName, "up",
"--no-deps", "-d", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestStartStopWithOneOffs(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-stop-with-oneoffs"
t.Run("Up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName,
"up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-bar-1 Started"), res.Combined())
})
t.Run("run one-off", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName, "run", "-d", "bar", "sleep", "infinity")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "-a")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-foo-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-run"), res.Combined())
})
t.Run("stop (not one-off containers)", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-foo-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "e2e_start_stop_with_oneoffs-bar-run"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "-a", "--status", "running")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-run"), res.Combined())
})
t.Run("start (not one-off containers)", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "start")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-foo-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-run"), res.Combined())
})
t.Run("restart (not one-off containers)", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "restart")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-foo-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar-run"), res.Combined())
})
t.Run("down", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "-a", "--status", "running")
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-with-oneoffs-bar"), res.Combined())
})
}
func TestStartAlreadyRunning(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-already-running",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
res := cli.RunDockerComposeCmd(t, "start", "simple")
assert.Equal(t, res.Stdout(), "", "No output should have been written to stdout")
}
func TestStopAlreadyStopped(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-already-stopped",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
// stop the container
cli.RunDockerComposeCmd(t, "stop", "simple")
// attempt to stop it again
res := cli.RunDockerComposeCmdNoCheck(t, "stop", "simple")
// TODO: for consistency, this should NOT write any output because the
// container is already stopped
res.Assert(t, icmd.Expected{
ExitCode: 0,
Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped",
})
}
func TestStartStopMultipleServices(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
res := cli.RunDockerComposeCmd(t, "stop", "simple", "another")
services := []string{"simple", "another"}
for _, svc := range services {
stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc)
assert.Assert(t, strings.Contains(res.Stderr(), stopMsg),
fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined()))
}
res = cli.RunDockerComposeCmd(t, "start", "simple", "another")
for _, svc := range services {
startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc)
assert.Assert(t, strings.Contains(res.Stderr(), startMsg),
fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined()))
}
}
func TestStartSingleServiceAndDependency(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-single-deps",
"COMPOSE_FILE=./fixtures/start-stop/start-stop-deps.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "create", "desired")
res := cli.RunDockerComposeCmd(t, "start", "desired")
desiredServices := []string{"desired", "dep_1", "dep_2"}
for _, s := range desiredServices {
startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s)
assert.Assert(t, strings.Contains(res.Combined(), startMsg),
fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined()))
}
undesiredServices := []string{"another", "another_2"}
for _, s := range undesiredServices {
assert.Assert(t, !strings.Contains(res.Combined(), s),
fmt.Sprintf("Shouldn't have message for service: %s\n%s", s, res.Combined()))
}
}
func TestStartStopMultipleFiles(t *testing.T) {
cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple-files"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "-p", "e2e-start-stop-svc-multiple-files", "down", "--remove-orphans")
})
cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d")
cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/other.yaml", "up", "-d")
res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined())
}
================================================
FILE: pkg/e2e/up_test.go
================================================
//go:build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"github.com/docker/compose/v5/pkg/utils"
)
func TestUpServiceUnhealthy(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-fail"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/compose.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container e2e-start-fail-fail-1 is unhealthy`})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
}
func TestUpDependenciesNotStopped(t *testing.T) {
c := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=up-deps-stop",
))
reset := func() {
c.RunDockerComposeCmdNoCheck(t, "down", "-t=0", "--remove-orphans", "-v")
}
reset()
t.Cleanup(reset)
t.Log("Launching orphan container (background)")
c.RunDockerComposeCmd(t,
"-f=./fixtures/ups-deps-stop/orphan.yaml",
"up",
"--wait",
"--detach",
"orphan",
)
RequireServiceState(t, c, "orphan", "running")
t.Log("Launching app container with implicit dependency")
upOut := &utils.SafeBuffer{}
testCmd := c.NewDockerComposeCmd(t,
"-f=./fixtures/ups-deps-stop/compose.yaml",
"up",
"--menu=false",
"app",
)
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
t.Cleanup(cancel)
cmd, err := StartWithNewGroupID(ctx, testCmd, upOut, nil)
assert.NilError(t, err, "Failed to run compose up")
t.Log("Waiting for containers to be in running state")
upOut.RequireEventuallyContains(t, "hello app")
RequireServiceState(t, c, "app", "running")
RequireServiceState(t, c, "dependency", "running")
t.Log("Simulating Ctrl-C")
require.NoError(t, syscall.Kill(-cmd.Process.Pid, syscall.SIGINT),
"Failed to send SIGINT to compose up process")
t.Log("Waiting for `compose up` to exit")
err = cmd.Wait()
if err != nil {
var exitErr *exec.ExitError
errors.As(err, &exitErr)
if exitErr.ExitCode() == -1 {
t.Fatalf("`compose up` was killed: %v", err)
}
require.Equal(t, 130, exitErr.ExitCode())
}
RequireServiceState(t, c, "app", "exited")
// dependency should still be running
RequireServiceState(t, c, "dependency", "running")
RequireServiceState(t, c, "orphan", "running")
}
func TestUpWithBuildDependencies(t *testing.T) {
c := NewParallelCLI(t)
t.Run("up with service using image build by an another service", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "built-image-dependency")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/dependencies",
"-f", "fixtures/dependencies/service-image-depends-on.yaml", "up", "-d")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/dependencies",
"-f", "fixtures/dependencies/service-image-depends-on.yaml", "down", "--rmi", "all")
})
res.Assert(t, icmd.Success)
})
}
func TestUpWithDependencyExit(t *testing.T) {
c := NewParallelCLI(t)
t.Run("up with dependency to exit before being healthy", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/dependencies",
"-f", "fixtures/dependencies/dependency-exit.yaml", "up", "-d")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", "dependencies", "down")
})
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "dependency failed to start: container dependencies-db-1 exited (1)"})
})
}
func TestScaleDoesntRecreate(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-scale"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "-d")
res := c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "--scale", "simple=2", "-d")
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
}
func TestUpWithDependencyNotRequired(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-dependency-not-required"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "--project-name", projectName,
"--profile", "not-required", "up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), " optional dependency \"bar\" failed to start"), res.Combined())
}
func TestUpWithAllResources(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-all-resources"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/resources/compose.yaml", "--all-resources", "--project-name", projectName, "up")
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())
}
func TestUpImageID(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-up-image-id"
digest := strings.TrimSpace(c.RunDockerCmd(t, "image", "inspect", "alpine", "-f", "{{ .ID }}").Stdout())
_, id, _ := strings.Cut(digest, ":")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v")
})
c = NewCLI(t, WithEnv(fmt.Sprintf("ID=%s", id)))
c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/id.yaml", "--project-name", projectName, "up")
}
func TestUpStopWithLogsMixed(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-stop-logs"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/stop/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit")
// assert we still get service2 logs after service 1 Stopped event
res.Assert(t, icmd.Expected{
Err: "Container compose-e2e-stop-logs-service1-1 Stopped",
})
// assert we get stop hook logs
res.Assert(t, icmd.Expected{Out: "service2-1 -> | stop hook running...\nservice2-1 | 64 bytes"})
}
================================================
FILE: pkg/e2e/volumes_test.go
================================================
/*
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 (
"fmt"
"net/http"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestLocalComposeVolume(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-volume"
t.Run("up with build and no image name, volume", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "compose-e2e-volume-nginx")
c.RunDockerOrExitError(t, "volume", "rm", projectName+"-staticVol")
c.RunDockerOrExitError(t, "volume", "rm", "myvolume")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/volume-test", "--project-name", projectName, "up",
"-d")
})
t.Run("access bind mount data", func(t *testing.T) {
output := HTTPGetWithRetry(t, "http://localhost:8090", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
})
t.Run("check container volume specs", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", "compose-e2e-volume-nginx2-1", "--format", "{{ json .Mounts }}")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, `"Destination":"/usr/src/app/node_modules","Driver":"local","Mode":"z","RW":true,"Propagation":""`), output)
assert.Assert(t, strings.Contains(output, `"Destination":"/myconfig","Mode":"","RW":false,"Propagation":"rprivate"`), output)
})
t.Run("check config content", func(t *testing.T) {
output := c.RunDockerCmd(t, "exec", "compose-e2e-volume-nginx2-1", "cat", "/myconfig").Stdout()
assert.Assert(t, strings.Contains(output, `Hello from Nginx container`), output)
})
t.Run("check secrets content", func(t *testing.T) {
output := c.RunDockerCmd(t, "exec", "compose-e2e-volume-nginx2-1", "cat", "/run/secrets/mysecret").Stdout()
assert.Assert(t, strings.Contains(output, `Hello from Nginx container`), output)
})
t.Run("check container bind-mounts specs", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", "compose-e2e-volume-nginx-1", "--format", "{{ json .Mounts }}")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, `"Type":"bind"`))
assert.Assert(t, strings.Contains(output, `"Destination":"/usr/share/nginx/html"`))
})
t.Run("should inherit anonymous volumes", func(t *testing.T) {
c.RunDockerOrExitError(t, "exec", "compose-e2e-volume-nginx2-1", "touch", "/usr/src/app/node_modules/test")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/volume-test", "--project-name", projectName, "up", "--force-recreate", "-d")
c.RunDockerOrExitError(t, "exec", "compose-e2e-volume-nginx2-1", "ls", "/usr/src/app/node_modules/test")
})
t.Run("should renew anonymous volumes", func(t *testing.T) {
c.RunDockerOrExitError(t, "exec", "compose-e2e-volume-nginx2-1", "touch", "/usr/src/app/node_modules/test")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/volume-test", "--project-name", projectName, "up", "--force-recreate", "--renew-anon-volumes", "-d")
c.RunDockerOrExitError(t, "exec", "compose-e2e-volume-nginx2-1", "ls", "/usr/src/app/node_modules/test")
})
t.Run("cleanup volume project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--volumes")
ls := c.RunDockerCmd(t, "volume", "ls").Stdout()
assert.Assert(t, !strings.Contains(ls, projectName+"-staticVol"))
assert.Assert(t, !strings.Contains(ls, "myvolume"))
})
}
func TestProjectVolumeBind(t *testing.T) {
if composeStandaloneMode {
t.Skip()
}
c := NewParallelCLI(t)
const projectName = "compose-e2e-project-volume-bind"
t.Run("up on project volume with bind specification", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Running on Windows. Skipping...")
}
tmpDir := t.TempDir()
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunDockerOrExitError(t, "volume", "rm", "-f", projectName+"_project-data").Assert(t, icmd.Success)
cmd := c.NewCmdWithEnv([]string{"TEST_DIR=" + tmpDir},
"docker", "compose", "--project-directory", "fixtures/project-volume-bind-test", "--project-name", projectName, "up", "-d")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
defer c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunCmd(t, "sh", "-c", "echo SUCCESS > "+filepath.Join(tmpDir, "resultfile")).Assert(t, icmd.Success)
ret := c.RunDockerOrExitError(t, "exec", "frontend", "bash", "-c", "cat /data/resultfile").Assert(t, icmd.Success)
assert.Assert(t, strings.Contains(ret.Stdout(), "SUCCESS"))
})
}
func TestUpSwitchVolumes(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-switch-volumes"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
c.RunDockerCmd(t, "volume", "rm", "-f", "test_external_volume")
c.RunDockerCmd(t, "volume", "rm", "-f", "test_external_volume_2")
})
c.RunDockerCmd(t, "volume", "create", "test_external_volume")
c.RunDockerCmd(t, "volume", "create", "test_external_volume_2")
c.RunDockerComposeCmd(t, "-f", "./fixtures/switch-volumes/compose.yaml", "--project-name", projectName, "up", "-d")
res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ (index .Mounts 0).Name }}")
res.Assert(t, icmd.Expected{Out: "test_external_volume"})
c.RunDockerComposeCmd(t, "-f", "./fixtures/switch-volumes/compose2.yaml", "--project-name", projectName, "up", "-d")
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ (index .Mounts 0).Name }}")
res.Assert(t, icmd.Expected{Out: "test_external_volume_2"})
}
func TestUpRecreateVolumes(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-recreate-volumes"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose.yaml", "--project-name", projectName, "up", "-d")
res := c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
res.Assert(t, icmd.Expected{Out: "bar"})
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose2.yaml", "--project-name", projectName, "up", "-d", "-y")
res = c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
res.Assert(t, icmd.Expected{Out: "zot"})
}
func TestUpRecreateVolumes_IgnoreBinds(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-recreate-volumes"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/bind.yaml", "--project-name", projectName, "up", "-d")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/bind.yaml", "--project-name", projectName, "up", "-d")
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
}
func TestImageVolume(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-image-volume"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
major, _, found := strings.Cut(version.Combined(), ".")
assert.Assert(t, found)
if major == "26" || major == "27" {
t.Skip("Skipping test due to docker version < 28")
}
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/volumes/compose.yaml", "--project-name", projectName, "up", "with_image")
out := res.Combined()
assert.Check(t, strings.Contains(out, "index.html"))
}
func TestImageVolumeRecreateOnRebuild(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-image-volume-recreate"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
c.RunDockerOrExitError(t, "rmi", "-f", "image-volume-source")
})
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
major, _, found := strings.Cut(version.Combined(), ".")
assert.Assert(t, found)
if major == "26" || major == "27" {
t.Skip("Skipping test due to docker version < 28")
}
// First build and run with initial content
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "build", "--build-arg", "CONTENT=foo")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "up", "-d")
assert.Check(t, !strings.Contains(res.Combined(), "error"))
// Check initial content
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "logs", "consumer")
assert.Check(t, strings.Contains(res.Combined(), "foo"), "Expected 'foo' in output, got: %s", res.Combined())
// Rebuild source image with different content
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "build", "--build-arg", "CONTENT=bar")
// Run up again - consumer should be recreated because source image changed
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "up", "-d")
// The consumer container should be recreated
assert.Check(t, strings.Contains(res.Combined(), "Recreate") || strings.Contains(res.Combined(), "Created"),
"Expected container to be recreated, got: %s", res.Combined())
// Check updated content
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
"--project-name", projectName, "logs", "consumer")
assert.Check(t, strings.Contains(res.Combined(), "bar"), "Expected 'bar' in output after rebuild, got: %s", res.Combined())
}
================================================
FILE: pkg/e2e/wait_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
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/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
}
func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
}
func TestWaitOnInfinity(t *testing.T) {
const projectName = "e2e-wait-infinity"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
cmd := c.NewDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
r := icmd.StartCmd(cmd)
assert.NilError(t, r.Error)
t.Cleanup(func() {
if r.Cmd.Process != nil {
_ = r.Cmd.Process.Kill()
}
})
finished := make(chan struct{})
ticker := time.NewTicker(7 * time.Second)
go func() {
_ = r.Cmd.Wait()
finished <- struct{}{}
}()
select {
case <-finished:
t.Fatal("wait infinity should not finish")
case <-ticker.C:
}
}
func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
}
================================================
FILE: pkg/e2e/watch_test.go
================================================
/*
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 (
"bytes"
"crypto/rand"
"fmt"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestWatch(t *testing.T) {
services := []string{"alpine", "busybox", "debian"}
for _, svcName := range services {
t.Run(svcName, func(t *testing.T) {
t.Helper()
doTest(t, svcName)
})
}
}
func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
const projectName = "test_rebuild_on_dotenv_with_external_network"
const svcName = "ext-alpine"
containerName := strings.Join([]string{projectName, svcName, "1"}, "-")
const networkName = "e2e-watch-external_network_test"
const dotEnvFilepath = "./fixtures/watch/.env"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName,
"COMPOSE_FILE=./fixtures/watch/with-external-network.yaml",
))
cleanup := func() {
c.RunDockerComposeCmdNoCheck(t, "down", "--remove-orphans", "--volumes", "--rmi=local")
c.RunDockerOrExitError(t, "network", "rm", networkName)
os.Remove(dotEnvFilepath) //nolint:errcheck
}
cleanup()
t.Log("create network that is referenced by the container we're testing")
c.RunDockerCmd(t, "network", "create", networkName)
res := c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
t.Log("create a dotenv file that will be used to trigger the rebuild")
err := os.WriteFile(dotEnvFilepath, []byte("HELLO=WORLD"), 0o666)
assert.NilError(t, err)
_, err = os.ReadFile(dotEnvFilepath)
assert.NilError(t, err)
// TODO: refactor this duplicated code into frameworks? Maybe?
t.Log("starting docker compose watch")
cmd := c.NewDockerComposeCmd(t, "--verbose", "watch", svcName)
// stream output since watch runs in the background
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
r := icmd.StartCmd(cmd)
require.NoError(t, r.Error)
var testComplete atomic.Bool
go func() {
// if the process exits abnormally before the test is done, fail the test
if err := r.Cmd.Wait(); err != nil && !t.Failed() && !testComplete.Load() {
assert.Check(t, cmp.Nil(err))
}
}()
t.Log("wait for watch to start watching")
c.WaitForCondition(t, func() (bool, string) {
out := r.String()
return strings.Contains(out, "Watch enabled"), "watch not started"
}, 30*time.Second, 1*time.Second)
pn := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
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)
assert.NilError(t, err)
_, err = os.ReadFile(dotEnvFilepath)
assert.NilError(t, err)
// NOTE: are there any other ways to check if the container has been rebuilt?
t.Log("check if the container has been rebuild")
c.WaitForCondition(t, func() (bool, string) {
out := r.String()
if strings.Count(out, "batch complete") != 1 {
return false, fmt.Sprintf("container %s was not rebuilt", containerName)
}
return true, fmt.Sprintf("container %s was rebuilt", containerName)
}, 30*time.Second, 1*time.Second)
pn2 := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
assert.Equal(t, strings.TrimSpace(pn2.Stdout()), networkName)
assert.Check(t, !strings.Contains(r.Combined(), "Application failed to start after update"))
t.Cleanup(cleanup)
t.Cleanup(func() {
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
if r.Cmd.Process != nil {
t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid)
_ = r.Cmd.Process.Kill()
}
})
testComplete.Store(true)
}
// NOTE: these tests all share a single Compose file but are safe to run
// concurrently (though that's not recommended).
func doTest(t *testing.T, svcName string) {
tmpdir := t.TempDir()
dataDir := filepath.Join(tmpdir, "data")
configDir := filepath.Join(tmpdir, "config")
writeTestFile := func(name, contents, sourceDir string) {
t.Helper()
dest := filepath.Join(sourceDir, name)
require.NoError(t, os.MkdirAll(filepath.Dir(dest), 0o700))
t.Logf("writing %q to %q", contents, dest)
require.NoError(t, os.WriteFile(dest, []byte(contents+"\n"), 0o600))
}
writeDataFile := func(name, contents string) {
writeTestFile(name, contents, dataDir)
}
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath)
projName := "e2e-watch-" + svcName
env := []string{
"COMPOSE_FILE=" + composeFilePath,
"COMPOSE_PROJECT_NAME=" + projName,
}
cli := NewCLI(t, WithEnv(env...))
// important that --rmi is used to prune the images and ensure that watch builds on launch
defer cli.cleanupWithDown(t, projName, "--rmi=local")
cmd := cli.NewDockerComposeCmd(t, "--verbose", "watch", svcName)
// stream output since watch runs in the background
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
r := icmd.StartCmd(cmd)
require.NoError(t, r.Error)
t.Cleanup(func() {
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
if r.Cmd.Process != nil {
t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid)
_ = r.Cmd.Process.Kill()
}
})
var testComplete atomic.Bool
go func() {
// if the process exits abnormally before the test is done, fail the test
if err := r.Cmd.Wait(); err != nil && !t.Failed() && !testComplete.Load() {
assert.Check(t, cmp.Nil(err))
}
}()
require.NoError(t, os.Mkdir(dataDir, 0o700))
checkFileContents := func(path string, contents string) poll.Check {
return func(pollLog poll.LogT) poll.Result {
if r.Cmd.ProcessState != nil {
return poll.Error(fmt.Errorf("watch process exited early: %s", r.Cmd.ProcessState))
}
res := icmd.RunCmd(cli.NewDockerComposeCmd(t, "exec", svcName, "cat", path))
if strings.Contains(res.Stdout(), contents) {
return poll.Success()
}
return poll.Continue("%v", res.Combined())
}
}
waitForFlush := func() {
b := make([]byte, 32)
_, _ = rand.Read(b)
sentinelVal := fmt.Sprintf("%x", b)
writeDataFile("wait.txt", sentinelVal)
poll.WaitOn(t, checkFileContents("/app/data/wait.txt", sentinelVal))
}
t.Logf("Writing to a file until Compose watch is up and running")
poll.WaitOn(t, func(t poll.LogT) poll.Result {
writeDataFile("hello.txt", "hello world")
return checkFileContents("/app/data/hello.txt", "hello world")(t)
}, poll.WithDelay(time.Second))
t.Logf("Modifying file contents")
writeDataFile("hello.txt", "hello watch")
poll.WaitOn(t, checkFileContents("/app/data/hello.txt", "hello watch"))
t.Logf("Deleting file")
require.NoError(t, os.Remove(filepath.Join(dataDir, "hello.txt")))
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/hello.txt").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
})
t.Logf("Writing to ignored paths")
writeDataFile("data.foo", "ignored")
writeDataFile(filepath.Join("ignored", "hello.txt"), "ignored")
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/data.foo").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
})
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
})
t.Logf("Creating subdirectory")
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
waitForFlush()
cli.RunDockerComposeCmd(t, "exec", svcName, "stat", "/app/data/subdir")
t.Logf("Writing to file in subdirectory")
writeDataFile(filepath.Join("subdir", "file.txt"), "a")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "a"))
t.Logf("Writing to file multiple times")
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "z"))
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "x"))
t.Logf("Deleting directory")
require.NoError(t, os.RemoveAll(filepath.Join(dataDir, "subdir")))
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/subdir").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
})
t.Logf("Sync and restart use case")
require.NoError(t, os.Mkdir(configDir, 0o700))
writeTestFile("file.config", "This is an updated config file", configDir)
checkRestart := func(state string) poll.Check {
return func(pollLog poll.LogT) poll.Result {
if strings.Contains(r.Combined(), state) {
return poll.Success()
}
return poll.Continue("%v", r.Combined())
}
}
poll.WaitOn(t, checkRestart(fmt.Sprintf("service(s) [%q] restarted", svcName)))
poll.WaitOn(t, checkFileContents("/app/config/file.config", "This is an updated config file"))
testComplete.Store(true)
}
func TestWatchExec(t *testing.T) {
c := NewCLI(t)
const projectName = "test_watch_exec"
defer c.cleanupWithDown(t, projectName)
tmpdir := t.TempDir()
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "exec.yaml"), composeFilePath)
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
poll.WaitOn(t, func(l poll.LogT) poll.Result {
out := buffer.String()
if strings.Contains(out, "64 bytes from") {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
t.Logf("Create new file")
testFile := filepath.Join(tmpdir, "test")
require.NoError(t, os.WriteFile(testFile, []byte("test\n"), 0o600))
poll.WaitOn(t, func(l poll.LogT) poll.Result {
out := buffer.String()
if strings.Contains(out, "SUCCESS") {
return poll.Success()
}
return poll.Continue("%v", out)
})
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}
func TestWatchMultiServices(t *testing.T) {
c := NewCLI(t)
const projectName = "test_watch_rebuild"
defer c.cleanupWithDown(t, projectName)
tmpdir := t.TempDir()
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "rebuild.yaml"), composeFilePath)
testFile := filepath.Join(tmpdir, "test")
require.NoError(t, os.WriteFile(testFile, []byte("test"), 0o600))
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
poll.WaitOn(t, func(l poll.LogT) poll.Result {
if strings.Contains(watch.Stdout(), "Attaching to ") {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
waitRebuild := func(service string, expected string) {
poll.WaitOn(t, func(l poll.LogT) poll.Result {
cat := c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "exec", service, "cat", "/data/"+service)
if strings.Contains(cat.Stdout(), expected) {
return poll.Success()
}
return poll.Continue("%v", cat.Combined())
})
}
waitRebuild("a", "test")
waitRebuild("b", "test")
waitRebuild("c", "test")
require.NoError(t, os.WriteFile(testFile, []byte("updated"), 0o600))
waitRebuild("a", "updated")
waitRebuild("b", "updated")
waitRebuild("c", "updated")
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}
func TestWatchIncludes(t *testing.T) {
c := NewCLI(t)
const projectName = "test_watch_includes"
defer c.cleanupWithDown(t, projectName)
tmpdir := t.TempDir()
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "include.yaml"), composeFilePath)
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
poll.WaitOn(t, func(l poll.LogT) poll.Result {
if strings.Contains(watch.Stdout(), "Attaching to ") {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "B.test"), []byte("test"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "A.test"), []byte("test"), 0o600))
poll.WaitOn(t, func(l poll.LogT) poll.Result {
cat := c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "exec", "a", "ls", "/data/")
if strings.Contains(cat.Stdout(), "A.test") {
assert.Check(t, !strings.Contains(cat.Stdout(), "B.test"))
return poll.Success()
}
return poll.Continue("%v", cat.Combined())
})
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}
func TestCheckWarningXInitialSyn(t *testing.T) {
c := NewCLI(t)
const projectName = "test_watch_warn_initial_syn"
defer c.cleanupWithDown(t, projectName)
tmpdir := t.TempDir()
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "x-initialSync.yaml"), composeFilePath)
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "--verbose", "up", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
poll.WaitOn(t, func(l poll.LogT) poll.Result {
if strings.Contains(watch.Combined(), "x-initialSync is DEPRECATED, please use the official `initial_sync` attribute") {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}
================================================
FILE: pkg/mocks/mock_docker_api.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/moby/moby/client (interfaces: APIClient)
//
// Generated by this command:
//
// mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/moby/moby/client APIClient
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
net "net"
reflect "reflect"
client "github.com/moby/moby/client"
gomock "go.uber.org/mock/gomock"
)
// MockAPIClient is a mock of APIClient interface.
type MockAPIClient struct {
ctrl *gomock.Controller
recorder *MockAPIClientMockRecorder
}
// MockAPIClientMockRecorder is the mock recorder for MockAPIClient.
type MockAPIClientMockRecorder struct {
mock *MockAPIClient
}
// NewMockAPIClient creates a new mock instance.
func NewMockAPIClient(ctrl *gomock.Controller) *MockAPIClient {
mock := &MockAPIClient{ctrl: ctrl}
mock.recorder = &MockAPIClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder {
return m.recorder
}
// BuildCachePrune mocks base method.
func (m *MockAPIClient) BuildCachePrune(arg0 context.Context, arg1 client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildCachePrune", arg0, arg1)
ret0, _ := ret[0].(client.BuildCachePruneResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildCachePrune indicates an expected call of BuildCachePrune.
func (mr *MockAPIClientMockRecorder) BuildCachePrune(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCachePrune", reflect.TypeOf((*MockAPIClient)(nil).BuildCachePrune), arg0, arg1)
}
// BuildCancel mocks base method.
func (m *MockAPIClient) BuildCancel(arg0 context.Context, arg1 string, arg2 client.BuildCancelOptions) (client.BuildCancelResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildCancel", arg0, arg1, arg2)
ret0, _ := ret[0].(client.BuildCancelResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildCancel indicates an expected call of BuildCancel.
func (mr *MockAPIClientMockRecorder) BuildCancel(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCancel", reflect.TypeOf((*MockAPIClient)(nil).BuildCancel), arg0, arg1, arg2)
}
// CheckpointCreate mocks base method.
func (m *MockAPIClient) CheckpointCreate(arg0 context.Context, arg1 string, arg2 client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckpointCreate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.CheckpointCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CheckpointCreate indicates an expected call of CheckpointCreate.
func (mr *MockAPIClientMockRecorder) CheckpointCreate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointCreate", reflect.TypeOf((*MockAPIClient)(nil).CheckpointCreate), arg0, arg1, arg2)
}
// CheckpointList mocks base method.
func (m *MockAPIClient) CheckpointList(arg0 context.Context, arg1 string, arg2 client.CheckpointListOptions) (client.CheckpointListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckpointList", arg0, arg1, arg2)
ret0, _ := ret[0].(client.CheckpointListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CheckpointList indicates an expected call of CheckpointList.
func (mr *MockAPIClientMockRecorder) CheckpointList(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointList", reflect.TypeOf((*MockAPIClient)(nil).CheckpointList), arg0, arg1, arg2)
}
// CheckpointRemove mocks base method.
func (m *MockAPIClient) CheckpointRemove(arg0 context.Context, arg1 string, arg2 client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckpointRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.CheckpointRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CheckpointRemove indicates an expected call of CheckpointRemove.
func (mr *MockAPIClientMockRecorder) CheckpointRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointRemove", reflect.TypeOf((*MockAPIClient)(nil).CheckpointRemove), arg0, arg1, arg2)
}
// ClientVersion mocks base method.
func (m *MockAPIClient) ClientVersion() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientVersion")
ret0, _ := ret[0].(string)
return ret0
}
// ClientVersion indicates an expected call of ClientVersion.
func (mr *MockAPIClientMockRecorder) ClientVersion() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientVersion", reflect.TypeOf((*MockAPIClient)(nil).ClientVersion))
}
// Close mocks base method.
func (m *MockAPIClient) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockAPIClientMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockAPIClient)(nil).Close))
}
// ConfigCreate mocks base method.
func (m *MockAPIClient) ConfigCreate(arg0 context.Context, arg1 client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigCreate", arg0, arg1)
ret0, _ := ret[0].(client.ConfigCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ConfigCreate indicates an expected call of ConfigCreate.
func (mr *MockAPIClientMockRecorder) ConfigCreate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigCreate", reflect.TypeOf((*MockAPIClient)(nil).ConfigCreate), arg0, arg1)
}
// ConfigInspect mocks base method.
func (m *MockAPIClient) ConfigInspect(arg0 context.Context, arg1 string, arg2 client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ConfigInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ConfigInspect indicates an expected call of ConfigInspect.
func (mr *MockAPIClientMockRecorder) ConfigInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigInspect", reflect.TypeOf((*MockAPIClient)(nil).ConfigInspect), arg0, arg1, arg2)
}
// ConfigList mocks base method.
func (m *MockAPIClient) ConfigList(arg0 context.Context, arg1 client.ConfigListOptions) (client.ConfigListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigList", arg0, arg1)
ret0, _ := ret[0].(client.ConfigListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ConfigList indicates an expected call of ConfigList.
func (mr *MockAPIClientMockRecorder) ConfigList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigList", reflect.TypeOf((*MockAPIClient)(nil).ConfigList), arg0, arg1)
}
// ConfigRemove mocks base method.
func (m *MockAPIClient) ConfigRemove(arg0 context.Context, arg1 string, arg2 client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ConfigRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ConfigRemove indicates an expected call of ConfigRemove.
func (mr *MockAPIClientMockRecorder) ConfigRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigRemove", reflect.TypeOf((*MockAPIClient)(nil).ConfigRemove), arg0, arg1, arg2)
}
// ConfigUpdate mocks base method.
func (m *MockAPIClient) ConfigUpdate(arg0 context.Context, arg1 string, arg2 client.ConfigUpdateOptions) (client.ConfigUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ConfigUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ConfigUpdate indicates an expected call of ConfigUpdate.
func (mr *MockAPIClientMockRecorder) ConfigUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigUpdate", reflect.TypeOf((*MockAPIClient)(nil).ConfigUpdate), arg0, arg1, arg2)
}
// ContainerAttach mocks base method.
func (m *MockAPIClient) ContainerAttach(arg0 context.Context, arg1 string, arg2 client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerAttach", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerAttachResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerAttach indicates an expected call of ContainerAttach.
func (mr *MockAPIClientMockRecorder) ContainerAttach(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerAttach", reflect.TypeOf((*MockAPIClient)(nil).ContainerAttach), arg0, arg1, arg2)
}
// ContainerCommit mocks base method.
func (m *MockAPIClient) ContainerCommit(arg0 context.Context, arg1 string, arg2 client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerCommit", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerCommitResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerCommit indicates an expected call of ContainerCommit.
func (mr *MockAPIClientMockRecorder) ContainerCommit(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerCommit", reflect.TypeOf((*MockAPIClient)(nil).ContainerCommit), arg0, arg1, arg2)
}
// ContainerCreate mocks base method.
func (m *MockAPIClient) ContainerCreate(arg0 context.Context, arg1 client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerCreate", arg0, arg1)
ret0, _ := ret[0].(client.ContainerCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerCreate indicates an expected call of ContainerCreate.
func (mr *MockAPIClientMockRecorder) ContainerCreate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerCreate", reflect.TypeOf((*MockAPIClient)(nil).ContainerCreate), arg0, arg1)
}
// ContainerDiff mocks base method.
func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string, arg2 client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerDiff", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerDiffResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerDiff indicates an expected call of ContainerDiff.
func (mr *MockAPIClientMockRecorder) ContainerDiff(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerDiff", reflect.TypeOf((*MockAPIClient)(nil).ContainerDiff), arg0, arg1, arg2)
}
// ContainerExport mocks base method.
func (m *MockAPIClient) ContainerExport(arg0 context.Context, arg1 string, arg2 client.ContainerExportOptions) (client.ContainerExportResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerExport", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerExportResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerExport indicates an expected call of ContainerExport.
func (mr *MockAPIClientMockRecorder) ContainerExport(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExport", reflect.TypeOf((*MockAPIClient)(nil).ContainerExport), arg0, arg1, arg2)
}
// ContainerInspect mocks base method.
func (m *MockAPIClient) ContainerInspect(arg0 context.Context, arg1 string, arg2 client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerInspect indicates an expected call of ContainerInspect.
func (mr *MockAPIClientMockRecorder) ContainerInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerInspect", reflect.TypeOf((*MockAPIClient)(nil).ContainerInspect), arg0, arg1, arg2)
}
// ContainerKill mocks base method.
func (m *MockAPIClient) ContainerKill(arg0 context.Context, arg1 string, arg2 client.ContainerKillOptions) (client.ContainerKillResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerKill", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerKillResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerKill indicates an expected call of ContainerKill.
func (mr *MockAPIClientMockRecorder) ContainerKill(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerKill", reflect.TypeOf((*MockAPIClient)(nil).ContainerKill), arg0, arg1, arg2)
}
// ContainerList mocks base method.
func (m *MockAPIClient) ContainerList(arg0 context.Context, arg1 client.ContainerListOptions) (client.ContainerListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerList", arg0, arg1)
ret0, _ := ret[0].(client.ContainerListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerList indicates an expected call of ContainerList.
func (mr *MockAPIClientMockRecorder) ContainerList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerList", reflect.TypeOf((*MockAPIClient)(nil).ContainerList), arg0, arg1)
}
// ContainerLogs mocks base method.
func (m *MockAPIClient) ContainerLogs(arg0 context.Context, arg1 string, arg2 client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerLogs", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerLogsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerLogs indicates an expected call of ContainerLogs.
func (mr *MockAPIClientMockRecorder) ContainerLogs(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerLogs", reflect.TypeOf((*MockAPIClient)(nil).ContainerLogs), arg0, arg1, arg2)
}
// ContainerPause mocks base method.
func (m *MockAPIClient) ContainerPause(arg0 context.Context, arg1 string, arg2 client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerPause", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerPauseResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerPause indicates an expected call of ContainerPause.
func (mr *MockAPIClientMockRecorder) ContainerPause(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerPause", reflect.TypeOf((*MockAPIClient)(nil).ContainerPause), arg0, arg1, arg2)
}
// ContainerPrune mocks base method.
func (m *MockAPIClient) ContainerPrune(arg0 context.Context, arg1 client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerPrune", arg0, arg1)
ret0, _ := ret[0].(client.ContainerPruneResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerPrune indicates an expected call of ContainerPrune.
func (mr *MockAPIClientMockRecorder) ContainerPrune(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerPrune", reflect.TypeOf((*MockAPIClient)(nil).ContainerPrune), arg0, arg1)
}
// ContainerRemove mocks base method.
func (m *MockAPIClient) ContainerRemove(arg0 context.Context, arg1 string, arg2 client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerRemove indicates an expected call of ContainerRemove.
func (mr *MockAPIClientMockRecorder) ContainerRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRemove", reflect.TypeOf((*MockAPIClient)(nil).ContainerRemove), arg0, arg1, arg2)
}
// ContainerRename mocks base method.
func (m *MockAPIClient) ContainerRename(arg0 context.Context, arg1 string, arg2 client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerRename", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerRenameResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerRename indicates an expected call of ContainerRename.
func (mr *MockAPIClientMockRecorder) ContainerRename(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRename", reflect.TypeOf((*MockAPIClient)(nil).ContainerRename), arg0, arg1, arg2)
}
// ContainerResize mocks base method.
func (m *MockAPIClient) ContainerResize(arg0 context.Context, arg1 string, arg2 client.ContainerResizeOptions) (client.ContainerResizeResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerResize", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerResizeResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerResize indicates an expected call of ContainerResize.
func (mr *MockAPIClientMockRecorder) ContainerResize(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerResize", reflect.TypeOf((*MockAPIClient)(nil).ContainerResize), arg0, arg1, arg2)
}
// ContainerRestart mocks base method.
func (m *MockAPIClient) ContainerRestart(arg0 context.Context, arg1 string, arg2 client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerRestart", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerRestartResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerRestart indicates an expected call of ContainerRestart.
func (mr *MockAPIClientMockRecorder) ContainerRestart(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRestart", reflect.TypeOf((*MockAPIClient)(nil).ContainerRestart), arg0, arg1, arg2)
}
// ContainerStart mocks base method.
func (m *MockAPIClient) ContainerStart(arg0 context.Context, arg1 string, arg2 client.ContainerStartOptions) (client.ContainerStartResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerStart", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerStartResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerStart indicates an expected call of ContainerStart.
func (mr *MockAPIClientMockRecorder) ContainerStart(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStart", reflect.TypeOf((*MockAPIClient)(nil).ContainerStart), arg0, arg1, arg2)
}
// ContainerStatPath mocks base method.
func (m *MockAPIClient) ContainerStatPath(arg0 context.Context, arg1 string, arg2 client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerStatPath", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerStatPathResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerStatPath indicates an expected call of ContainerStatPath.
func (mr *MockAPIClientMockRecorder) ContainerStatPath(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStatPath", reflect.TypeOf((*MockAPIClient)(nil).ContainerStatPath), arg0, arg1, arg2)
}
// ContainerStats mocks base method.
func (m *MockAPIClient) ContainerStats(arg0 context.Context, arg1 string, arg2 client.ContainerStatsOptions) (client.ContainerStatsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerStats", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerStatsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerStats indicates an expected call of ContainerStats.
func (mr *MockAPIClientMockRecorder) ContainerStats(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStats", reflect.TypeOf((*MockAPIClient)(nil).ContainerStats), arg0, arg1, arg2)
}
// ContainerStop mocks base method.
func (m *MockAPIClient) ContainerStop(arg0 context.Context, arg1 string, arg2 client.ContainerStopOptions) (client.ContainerStopResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerStop", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerStopResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerStop indicates an expected call of ContainerStop.
func (mr *MockAPIClientMockRecorder) ContainerStop(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStop", reflect.TypeOf((*MockAPIClient)(nil).ContainerStop), arg0, arg1, arg2)
}
// ContainerTop mocks base method.
func (m *MockAPIClient) ContainerTop(arg0 context.Context, arg1 string, arg2 client.ContainerTopOptions) (client.ContainerTopResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerTop", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerTopResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerTop indicates an expected call of ContainerTop.
func (mr *MockAPIClientMockRecorder) ContainerTop(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerTop", reflect.TypeOf((*MockAPIClient)(nil).ContainerTop), arg0, arg1, arg2)
}
// ContainerUnpause mocks base method.
func (m *MockAPIClient) ContainerUnpause(arg0 context.Context, arg1 string, arg2 client.ContainerUnpauseOptions) (client.ContainerUnpauseResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerUnpause", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerUnpauseResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerUnpause indicates an expected call of ContainerUnpause.
func (mr *MockAPIClientMockRecorder) ContainerUnpause(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerUnpause", reflect.TypeOf((*MockAPIClient)(nil).ContainerUnpause), arg0, arg1, arg2)
}
// ContainerUpdate mocks base method.
func (m *MockAPIClient) ContainerUpdate(arg0 context.Context, arg1 string, arg2 client.ContainerUpdateOptions) (client.ContainerUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContainerUpdate indicates an expected call of ContainerUpdate.
func (mr *MockAPIClientMockRecorder) ContainerUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerUpdate", reflect.TypeOf((*MockAPIClient)(nil).ContainerUpdate), arg0, arg1, arg2)
}
// ContainerWait mocks base method.
func (m *MockAPIClient) ContainerWait(arg0 context.Context, arg1 string, arg2 client.ContainerWaitOptions) client.ContainerWaitResult {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerWait", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ContainerWaitResult)
return ret0
}
// ContainerWait indicates an expected call of ContainerWait.
func (mr *MockAPIClientMockRecorder) ContainerWait(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerWait", reflect.TypeOf((*MockAPIClient)(nil).ContainerWait), arg0, arg1, arg2)
}
// CopyFromContainer mocks base method.
func (m *MockAPIClient) CopyFromContainer(arg0 context.Context, arg1 string, arg2 client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CopyFromContainer", arg0, arg1, arg2)
ret0, _ := ret[0].(client.CopyFromContainerResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CopyFromContainer indicates an expected call of CopyFromContainer.
func (mr *MockAPIClientMockRecorder) CopyFromContainer(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFromContainer", reflect.TypeOf((*MockAPIClient)(nil).CopyFromContainer), arg0, arg1, arg2)
}
// CopyToContainer mocks base method.
func (m *MockAPIClient) CopyToContainer(arg0 context.Context, arg1 string, arg2 client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CopyToContainer", arg0, arg1, arg2)
ret0, _ := ret[0].(client.CopyToContainerResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CopyToContainer indicates an expected call of CopyToContainer.
func (mr *MockAPIClientMockRecorder) CopyToContainer(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyToContainer", reflect.TypeOf((*MockAPIClient)(nil).CopyToContainer), arg0, arg1, arg2)
}
// DaemonHost mocks base method.
func (m *MockAPIClient) DaemonHost() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DaemonHost")
ret0, _ := ret[0].(string)
return ret0
}
// DaemonHost indicates an expected call of DaemonHost.
func (mr *MockAPIClientMockRecorder) DaemonHost() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DaemonHost", reflect.TypeOf((*MockAPIClient)(nil).DaemonHost))
}
// DialHijack mocks base method.
func (m *MockAPIClient) DialHijack(arg0 context.Context, arg1, arg2 string, arg3 map[string][]string) (net.Conn, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialHijack", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(net.Conn)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DialHijack indicates an expected call of DialHijack.
func (mr *MockAPIClientMockRecorder) DialHijack(arg0, arg1, arg2, arg3 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialHijack", reflect.TypeOf((*MockAPIClient)(nil).DialHijack), arg0, arg1, arg2, arg3)
}
// Dialer mocks base method.
func (m *MockAPIClient) Dialer() func(context.Context) (net.Conn, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dialer")
ret0, _ := ret[0].(func(context.Context) (net.Conn, error))
return ret0
}
// Dialer indicates an expected call of Dialer.
func (mr *MockAPIClientMockRecorder) Dialer() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dialer", reflect.TypeOf((*MockAPIClient)(nil).Dialer))
}
// DiskUsage mocks base method.
func (m *MockAPIClient) DiskUsage(arg0 context.Context, arg1 client.DiskUsageOptions) (client.DiskUsageResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DiskUsage", arg0, arg1)
ret0, _ := ret[0].(client.DiskUsageResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DiskUsage indicates an expected call of DiskUsage.
func (mr *MockAPIClientMockRecorder) DiskUsage(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiskUsage", reflect.TypeOf((*MockAPIClient)(nil).DiskUsage), arg0, arg1)
}
// DistributionInspect mocks base method.
func (m *MockAPIClient) DistributionInspect(arg0 context.Context, arg1 string, arg2 client.DistributionInspectOptions) (client.DistributionInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DistributionInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.DistributionInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DistributionInspect indicates an expected call of DistributionInspect.
func (mr *MockAPIClientMockRecorder) DistributionInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DistributionInspect", reflect.TypeOf((*MockAPIClient)(nil).DistributionInspect), arg0, arg1, arg2)
}
// Events mocks base method.
func (m *MockAPIClient) Events(arg0 context.Context, arg1 client.EventsListOptions) client.EventsResult {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Events", arg0, arg1)
ret0, _ := ret[0].(client.EventsResult)
return ret0
}
// Events indicates an expected call of Events.
func (mr *MockAPIClientMockRecorder) Events(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockAPIClient)(nil).Events), arg0, arg1)
}
// ExecAttach mocks base method.
func (m *MockAPIClient) ExecAttach(arg0 context.Context, arg1 string, arg2 client.ExecAttachOptions) (client.ExecAttachResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecAttach", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ExecAttachResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecAttach indicates an expected call of ExecAttach.
func (mr *MockAPIClientMockRecorder) ExecAttach(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecAttach", reflect.TypeOf((*MockAPIClient)(nil).ExecAttach), arg0, arg1, arg2)
}
// ExecCreate mocks base method.
func (m *MockAPIClient) ExecCreate(arg0 context.Context, arg1 string, arg2 client.ExecCreateOptions) (client.ExecCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecCreate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ExecCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCreate indicates an expected call of ExecCreate.
func (mr *MockAPIClientMockRecorder) ExecCreate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCreate", reflect.TypeOf((*MockAPIClient)(nil).ExecCreate), arg0, arg1, arg2)
}
// ExecInspect mocks base method.
func (m *MockAPIClient) ExecInspect(arg0 context.Context, arg1 string, arg2 client.ExecInspectOptions) (client.ExecInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ExecInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecInspect indicates an expected call of ExecInspect.
func (mr *MockAPIClientMockRecorder) ExecInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecInspect", reflect.TypeOf((*MockAPIClient)(nil).ExecInspect), arg0, arg1, arg2)
}
// ExecResize mocks base method.
func (m *MockAPIClient) ExecResize(arg0 context.Context, arg1 string, arg2 client.ExecResizeOptions) (client.ExecResizeResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecResize", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ExecResizeResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecResize indicates an expected call of ExecResize.
func (mr *MockAPIClientMockRecorder) ExecResize(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecResize", reflect.TypeOf((*MockAPIClient)(nil).ExecResize), arg0, arg1, arg2)
}
// ExecStart mocks base method.
func (m *MockAPIClient) ExecStart(arg0 context.Context, arg1 string, arg2 client.ExecStartOptions) (client.ExecStartResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecStart", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ExecStartResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecStart indicates an expected call of ExecStart.
func (mr *MockAPIClientMockRecorder) ExecStart(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecStart", reflect.TypeOf((*MockAPIClient)(nil).ExecStart), arg0, arg1, arg2)
}
// ImageBuild mocks base method.
func (m *MockAPIClient) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 client.ImageBuildOptions) (client.ImageBuildResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageBuild", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ImageBuildResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageBuild indicates an expected call of ImageBuild.
func (mr *MockAPIClientMockRecorder) ImageBuild(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageBuild", reflect.TypeOf((*MockAPIClient)(nil).ImageBuild), arg0, arg1, arg2)
}
// ImageHistory mocks base method.
func (m *MockAPIClient) ImageHistory(arg0 context.Context, arg1 string, arg2 ...client.ImageHistoryOption) (client.ImageHistoryResult, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ImageHistory", varargs...)
ret0, _ := ret[0].(client.ImageHistoryResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageHistory indicates an expected call of ImageHistory.
func (mr *MockAPIClientMockRecorder) ImageHistory(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageHistory", reflect.TypeOf((*MockAPIClient)(nil).ImageHistory), varargs...)
}
// ImageImport mocks base method.
func (m *MockAPIClient) ImageImport(arg0 context.Context, arg1 client.ImageImportSource, arg2 string, arg3 client.ImageImportOptions) (client.ImageImportResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageImport", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(client.ImageImportResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageImport indicates an expected call of ImageImport.
func (mr *MockAPIClientMockRecorder) ImageImport(arg0, arg1, arg2, arg3 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageImport", reflect.TypeOf((*MockAPIClient)(nil).ImageImport), arg0, arg1, arg2, arg3)
}
// ImageInspect mocks base method.
func (m *MockAPIClient) ImageInspect(arg0 context.Context, arg1 string, arg2 ...client.ImageInspectOption) (client.ImageInspectResult, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ImageInspect", varargs...)
ret0, _ := ret[0].(client.ImageInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageInspect indicates an expected call of ImageInspect.
func (mr *MockAPIClientMockRecorder) ImageInspect(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageInspect", reflect.TypeOf((*MockAPIClient)(nil).ImageInspect), varargs...)
}
// ImageList mocks base method.
func (m *MockAPIClient) ImageList(arg0 context.Context, arg1 client.ImageListOptions) (client.ImageListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageList", arg0, arg1)
ret0, _ := ret[0].(client.ImageListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageList indicates an expected call of ImageList.
func (mr *MockAPIClientMockRecorder) ImageList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageList", reflect.TypeOf((*MockAPIClient)(nil).ImageList), arg0, arg1)
}
// ImageLoad mocks base method.
func (m *MockAPIClient) ImageLoad(arg0 context.Context, arg1 io.Reader, arg2 ...client.ImageLoadOption) (client.ImageLoadResult, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ImageLoad", varargs...)
ret0, _ := ret[0].(client.ImageLoadResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageLoad indicates an expected call of ImageLoad.
func (mr *MockAPIClientMockRecorder) ImageLoad(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageLoad", reflect.TypeOf((*MockAPIClient)(nil).ImageLoad), varargs...)
}
// ImagePrune mocks base method.
func (m *MockAPIClient) ImagePrune(arg0 context.Context, arg1 client.ImagePruneOptions) (client.ImagePruneResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImagePrune", arg0, arg1)
ret0, _ := ret[0].(client.ImagePruneResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImagePrune indicates an expected call of ImagePrune.
func (mr *MockAPIClientMockRecorder) ImagePrune(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePrune", reflect.TypeOf((*MockAPIClient)(nil).ImagePrune), arg0, arg1)
}
// ImagePull mocks base method.
func (m *MockAPIClient) ImagePull(arg0 context.Context, arg1 string, arg2 client.ImagePullOptions) (client.ImagePullResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImagePull", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ImagePullResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImagePull indicates an expected call of ImagePull.
func (mr *MockAPIClientMockRecorder) ImagePull(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePull", reflect.TypeOf((*MockAPIClient)(nil).ImagePull), arg0, arg1, arg2)
}
// ImagePush mocks base method.
func (m *MockAPIClient) ImagePush(arg0 context.Context, arg1 string, arg2 client.ImagePushOptions) (client.ImagePushResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImagePush", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ImagePushResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImagePush indicates an expected call of ImagePush.
func (mr *MockAPIClientMockRecorder) ImagePush(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePush", reflect.TypeOf((*MockAPIClient)(nil).ImagePush), arg0, arg1, arg2)
}
// ImageRemove mocks base method.
func (m *MockAPIClient) ImageRemove(arg0 context.Context, arg1 string, arg2 client.ImageRemoveOptions) (client.ImageRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ImageRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageRemove indicates an expected call of ImageRemove.
func (mr *MockAPIClientMockRecorder) ImageRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageRemove", reflect.TypeOf((*MockAPIClient)(nil).ImageRemove), arg0, arg1, arg2)
}
// ImageSave mocks base method.
func (m *MockAPIClient) ImageSave(arg0 context.Context, arg1 []string, arg2 ...client.ImageSaveOption) (client.ImageSaveResult, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ImageSave", varargs...)
ret0, _ := ret[0].(client.ImageSaveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageSave indicates an expected call of ImageSave.
func (mr *MockAPIClientMockRecorder) ImageSave(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageSave", reflect.TypeOf((*MockAPIClient)(nil).ImageSave), varargs...)
}
// ImageSearch mocks base method.
func (m *MockAPIClient) ImageSearch(arg0 context.Context, arg1 string, arg2 client.ImageSearchOptions) (client.ImageSearchResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageSearch", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ImageSearchResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageSearch indicates an expected call of ImageSearch.
func (mr *MockAPIClientMockRecorder) ImageSearch(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageSearch", reflect.TypeOf((*MockAPIClient)(nil).ImageSearch), arg0, arg1, arg2)
}
// ImageTag mocks base method.
func (m *MockAPIClient) ImageTag(arg0 context.Context, arg1 client.ImageTagOptions) (client.ImageTagResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImageTag", arg0, arg1)
ret0, _ := ret[0].(client.ImageTagResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImageTag indicates an expected call of ImageTag.
func (mr *MockAPIClientMockRecorder) ImageTag(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageTag", reflect.TypeOf((*MockAPIClient)(nil).ImageTag), arg0, arg1)
}
// Info mocks base method.
func (m *MockAPIClient) Info(arg0 context.Context, arg1 client.InfoOptions) (client.SystemInfoResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info", arg0, arg1)
ret0, _ := ret[0].(client.SystemInfoResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockAPIClientMockRecorder) Info(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockAPIClient)(nil).Info), arg0, arg1)
}
// NetworkConnect mocks base method.
func (m *MockAPIClient) NetworkConnect(arg0 context.Context, arg1 string, arg2 client.NetworkConnectOptions) (client.NetworkConnectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkConnect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NetworkConnectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkConnect indicates an expected call of NetworkConnect.
func (mr *MockAPIClientMockRecorder) NetworkConnect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkConnect", reflect.TypeOf((*MockAPIClient)(nil).NetworkConnect), arg0, arg1, arg2)
}
// NetworkCreate mocks base method.
func (m *MockAPIClient) NetworkCreate(arg0 context.Context, arg1 string, arg2 client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkCreate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NetworkCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkCreate indicates an expected call of NetworkCreate.
func (mr *MockAPIClientMockRecorder) NetworkCreate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkCreate", reflect.TypeOf((*MockAPIClient)(nil).NetworkCreate), arg0, arg1, arg2)
}
// NetworkDisconnect mocks base method.
func (m *MockAPIClient) NetworkDisconnect(arg0 context.Context, arg1 string, arg2 client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkDisconnect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NetworkDisconnectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkDisconnect indicates an expected call of NetworkDisconnect.
func (mr *MockAPIClientMockRecorder) NetworkDisconnect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkDisconnect", reflect.TypeOf((*MockAPIClient)(nil).NetworkDisconnect), arg0, arg1, arg2)
}
// NetworkInspect mocks base method.
func (m *MockAPIClient) NetworkInspect(arg0 context.Context, arg1 string, arg2 client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NetworkInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkInspect indicates an expected call of NetworkInspect.
func (mr *MockAPIClientMockRecorder) NetworkInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkInspect", reflect.TypeOf((*MockAPIClient)(nil).NetworkInspect), arg0, arg1, arg2)
}
// NetworkList mocks base method.
func (m *MockAPIClient) NetworkList(arg0 context.Context, arg1 client.NetworkListOptions) (client.NetworkListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkList", arg0, arg1)
ret0, _ := ret[0].(client.NetworkListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkList indicates an expected call of NetworkList.
func (mr *MockAPIClientMockRecorder) NetworkList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkList", reflect.TypeOf((*MockAPIClient)(nil).NetworkList), arg0, arg1)
}
// NetworkPrune mocks base method.
func (m *MockAPIClient) NetworkPrune(arg0 context.Context, arg1 client.NetworkPruneOptions) (client.NetworkPruneResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkPrune", arg0, arg1)
ret0, _ := ret[0].(client.NetworkPruneResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkPrune indicates an expected call of NetworkPrune.
func (mr *MockAPIClientMockRecorder) NetworkPrune(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkPrune", reflect.TypeOf((*MockAPIClient)(nil).NetworkPrune), arg0, arg1)
}
// NetworkRemove mocks base method.
func (m *MockAPIClient) NetworkRemove(arg0 context.Context, arg1 string, arg2 client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NetworkRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NetworkRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NetworkRemove indicates an expected call of NetworkRemove.
func (mr *MockAPIClientMockRecorder) NetworkRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkRemove", reflect.TypeOf((*MockAPIClient)(nil).NetworkRemove), arg0, arg1, arg2)
}
// NodeInspect mocks base method.
func (m *MockAPIClient) NodeInspect(arg0 context.Context, arg1 string, arg2 client.NodeInspectOptions) (client.NodeInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NodeInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NodeInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeInspect indicates an expected call of NodeInspect.
func (mr *MockAPIClientMockRecorder) NodeInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeInspect", reflect.TypeOf((*MockAPIClient)(nil).NodeInspect), arg0, arg1, arg2)
}
// NodeList mocks base method.
func (m *MockAPIClient) NodeList(arg0 context.Context, arg1 client.NodeListOptions) (client.NodeListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NodeList", arg0, arg1)
ret0, _ := ret[0].(client.NodeListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeList indicates an expected call of NodeList.
func (mr *MockAPIClientMockRecorder) NodeList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeList", reflect.TypeOf((*MockAPIClient)(nil).NodeList), arg0, arg1)
}
// NodeRemove mocks base method.
func (m *MockAPIClient) NodeRemove(arg0 context.Context, arg1 string, arg2 client.NodeRemoveOptions) (client.NodeRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NodeRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NodeRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeRemove indicates an expected call of NodeRemove.
func (mr *MockAPIClientMockRecorder) NodeRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRemove", reflect.TypeOf((*MockAPIClient)(nil).NodeRemove), arg0, arg1, arg2)
}
// NodeUpdate mocks base method.
func (m *MockAPIClient) NodeUpdate(arg0 context.Context, arg1 string, arg2 client.NodeUpdateOptions) (client.NodeUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NodeUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.NodeUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeUpdate indicates an expected call of NodeUpdate.
func (mr *MockAPIClientMockRecorder) NodeUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeUpdate", reflect.TypeOf((*MockAPIClient)(nil).NodeUpdate), arg0, arg1, arg2)
}
// Ping mocks base method.
func (m *MockAPIClient) Ping(arg0 context.Context, arg1 client.PingOptions) (client.PingResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", arg0, arg1)
ret0, _ := ret[0].(client.PingResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ping indicates an expected call of Ping.
func (mr *MockAPIClientMockRecorder) Ping(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockAPIClient)(nil).Ping), arg0, arg1)
}
// PluginCreate mocks base method.
func (m *MockAPIClient) PluginCreate(arg0 context.Context, arg1 io.Reader, arg2 client.PluginCreateOptions) (client.PluginCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginCreate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginCreate indicates an expected call of PluginCreate.
func (mr *MockAPIClientMockRecorder) PluginCreate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginCreate", reflect.TypeOf((*MockAPIClient)(nil).PluginCreate), arg0, arg1, arg2)
}
// PluginDisable mocks base method.
func (m *MockAPIClient) PluginDisable(arg0 context.Context, arg1 string, arg2 client.PluginDisableOptions) (client.PluginDisableResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginDisable", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginDisableResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginDisable indicates an expected call of PluginDisable.
func (mr *MockAPIClientMockRecorder) PluginDisable(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginDisable", reflect.TypeOf((*MockAPIClient)(nil).PluginDisable), arg0, arg1, arg2)
}
// PluginEnable mocks base method.
func (m *MockAPIClient) PluginEnable(arg0 context.Context, arg1 string, arg2 client.PluginEnableOptions) (client.PluginEnableResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginEnable", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginEnableResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginEnable indicates an expected call of PluginEnable.
func (mr *MockAPIClientMockRecorder) PluginEnable(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginEnable", reflect.TypeOf((*MockAPIClient)(nil).PluginEnable), arg0, arg1, arg2)
}
// PluginInspect mocks base method.
func (m *MockAPIClient) PluginInspect(arg0 context.Context, arg1 string, arg2 client.PluginInspectOptions) (client.PluginInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginInspect indicates an expected call of PluginInspect.
func (mr *MockAPIClientMockRecorder) PluginInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginInspect", reflect.TypeOf((*MockAPIClient)(nil).PluginInspect), arg0, arg1, arg2)
}
// PluginInstall mocks base method.
func (m *MockAPIClient) PluginInstall(arg0 context.Context, arg1 string, arg2 client.PluginInstallOptions) (client.PluginInstallResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginInstall", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginInstallResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginInstall indicates an expected call of PluginInstall.
func (mr *MockAPIClientMockRecorder) PluginInstall(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginInstall", reflect.TypeOf((*MockAPIClient)(nil).PluginInstall), arg0, arg1, arg2)
}
// PluginList mocks base method.
func (m *MockAPIClient) PluginList(arg0 context.Context, arg1 client.PluginListOptions) (client.PluginListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginList", arg0, arg1)
ret0, _ := ret[0].(client.PluginListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginList indicates an expected call of PluginList.
func (mr *MockAPIClientMockRecorder) PluginList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginList", reflect.TypeOf((*MockAPIClient)(nil).PluginList), arg0, arg1)
}
// PluginPush mocks base method.
func (m *MockAPIClient) PluginPush(arg0 context.Context, arg1 string, arg2 client.PluginPushOptions) (client.PluginPushResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginPush", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginPushResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginPush indicates an expected call of PluginPush.
func (mr *MockAPIClientMockRecorder) PluginPush(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginPush", reflect.TypeOf((*MockAPIClient)(nil).PluginPush), arg0, arg1, arg2)
}
// PluginRemove mocks base method.
func (m *MockAPIClient) PluginRemove(arg0 context.Context, arg1 string, arg2 client.PluginRemoveOptions) (client.PluginRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginRemove indicates an expected call of PluginRemove.
func (mr *MockAPIClientMockRecorder) PluginRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginRemove", reflect.TypeOf((*MockAPIClient)(nil).PluginRemove), arg0, arg1, arg2)
}
// PluginSet mocks base method.
func (m *MockAPIClient) PluginSet(arg0 context.Context, arg1 string, arg2 client.PluginSetOptions) (client.PluginSetResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginSet", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginSetResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginSet indicates an expected call of PluginSet.
func (mr *MockAPIClientMockRecorder) PluginSet(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginSet", reflect.TypeOf((*MockAPIClient)(nil).PluginSet), arg0, arg1, arg2)
}
// PluginUpgrade mocks base method.
func (m *MockAPIClient) PluginUpgrade(arg0 context.Context, arg1 string, arg2 client.PluginUpgradeOptions) (client.PluginUpgradeResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginUpgrade", arg0, arg1, arg2)
ret0, _ := ret[0].(client.PluginUpgradeResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginUpgrade indicates an expected call of PluginUpgrade.
func (mr *MockAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginUpgrade", reflect.TypeOf((*MockAPIClient)(nil).PluginUpgrade), arg0, arg1, arg2)
}
// RegistryLogin mocks base method.
func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 client.RegistryLoginOptions) (client.RegistryLoginResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegistryLogin", arg0, arg1)
ret0, _ := ret[0].(client.RegistryLoginResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RegistryLogin indicates an expected call of RegistryLogin.
func (mr *MockAPIClientMockRecorder) RegistryLogin(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryLogin", reflect.TypeOf((*MockAPIClient)(nil).RegistryLogin), arg0, arg1)
}
// SecretCreate mocks base method.
func (m *MockAPIClient) SecretCreate(arg0 context.Context, arg1 client.SecretCreateOptions) (client.SecretCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecretCreate", arg0, arg1)
ret0, _ := ret[0].(client.SecretCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SecretCreate indicates an expected call of SecretCreate.
func (mr *MockAPIClientMockRecorder) SecretCreate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretCreate", reflect.TypeOf((*MockAPIClient)(nil).SecretCreate), arg0, arg1)
}
// SecretInspect mocks base method.
func (m *MockAPIClient) SecretInspect(arg0 context.Context, arg1 string, arg2 client.SecretInspectOptions) (client.SecretInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecretInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.SecretInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SecretInspect indicates an expected call of SecretInspect.
func (mr *MockAPIClientMockRecorder) SecretInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretInspect", reflect.TypeOf((*MockAPIClient)(nil).SecretInspect), arg0, arg1, arg2)
}
// SecretList mocks base method.
func (m *MockAPIClient) SecretList(arg0 context.Context, arg1 client.SecretListOptions) (client.SecretListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecretList", arg0, arg1)
ret0, _ := ret[0].(client.SecretListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SecretList indicates an expected call of SecretList.
func (mr *MockAPIClientMockRecorder) SecretList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretList", reflect.TypeOf((*MockAPIClient)(nil).SecretList), arg0, arg1)
}
// SecretRemove mocks base method.
func (m *MockAPIClient) SecretRemove(arg0 context.Context, arg1 string, arg2 client.SecretRemoveOptions) (client.SecretRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecretRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.SecretRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SecretRemove indicates an expected call of SecretRemove.
func (mr *MockAPIClientMockRecorder) SecretRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretRemove", reflect.TypeOf((*MockAPIClient)(nil).SecretRemove), arg0, arg1, arg2)
}
// SecretUpdate mocks base method.
func (m *MockAPIClient) SecretUpdate(arg0 context.Context, arg1 string, arg2 client.SecretUpdateOptions) (client.SecretUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecretUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.SecretUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SecretUpdate indicates an expected call of SecretUpdate.
func (mr *MockAPIClientMockRecorder) SecretUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretUpdate", reflect.TypeOf((*MockAPIClient)(nil).SecretUpdate), arg0, arg1, arg2)
}
// ServerVersion mocks base method.
func (m *MockAPIClient) ServerVersion(arg0 context.Context, arg1 client.ServerVersionOptions) (client.ServerVersionResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerVersion", arg0, arg1)
ret0, _ := ret[0].(client.ServerVersionResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServerVersion indicates an expected call of ServerVersion.
func (mr *MockAPIClientMockRecorder) ServerVersion(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerVersion", reflect.TypeOf((*MockAPIClient)(nil).ServerVersion), arg0, arg1)
}
// ServiceCreate mocks base method.
func (m *MockAPIClient) ServiceCreate(arg0 context.Context, arg1 client.ServiceCreateOptions) (client.ServiceCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceCreate", arg0, arg1)
ret0, _ := ret[0].(client.ServiceCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceCreate indicates an expected call of ServiceCreate.
func (mr *MockAPIClientMockRecorder) ServiceCreate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceCreate", reflect.TypeOf((*MockAPIClient)(nil).ServiceCreate), arg0, arg1)
}
// ServiceInspect mocks base method.
func (m *MockAPIClient) ServiceInspect(arg0 context.Context, arg1 string, arg2 client.ServiceInspectOptions) (client.ServiceInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ServiceInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceInspect indicates an expected call of ServiceInspect.
func (mr *MockAPIClientMockRecorder) ServiceInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceInspect", reflect.TypeOf((*MockAPIClient)(nil).ServiceInspect), arg0, arg1, arg2)
}
// ServiceList mocks base method.
func (m *MockAPIClient) ServiceList(arg0 context.Context, arg1 client.ServiceListOptions) (client.ServiceListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceList", arg0, arg1)
ret0, _ := ret[0].(client.ServiceListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceList indicates an expected call of ServiceList.
func (mr *MockAPIClientMockRecorder) ServiceList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceList", reflect.TypeOf((*MockAPIClient)(nil).ServiceList), arg0, arg1)
}
// ServiceLogs mocks base method.
func (m *MockAPIClient) ServiceLogs(arg0 context.Context, arg1 string, arg2 client.ServiceLogsOptions) (client.ServiceLogsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceLogs", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ServiceLogsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceLogs indicates an expected call of ServiceLogs.
func (mr *MockAPIClientMockRecorder) ServiceLogs(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceLogs", reflect.TypeOf((*MockAPIClient)(nil).ServiceLogs), arg0, arg1, arg2)
}
// ServiceRemove mocks base method.
func (m *MockAPIClient) ServiceRemove(arg0 context.Context, arg1 string, arg2 client.ServiceRemoveOptions) (client.ServiceRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ServiceRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceRemove indicates an expected call of ServiceRemove.
func (mr *MockAPIClientMockRecorder) ServiceRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceRemove", reflect.TypeOf((*MockAPIClient)(nil).ServiceRemove), arg0, arg1, arg2)
}
// ServiceUpdate mocks base method.
func (m *MockAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServiceUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.ServiceUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServiceUpdate indicates an expected call of ServiceUpdate.
func (mr *MockAPIClientMockRecorder) ServiceUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceUpdate", reflect.TypeOf((*MockAPIClient)(nil).ServiceUpdate), arg0, arg1, arg2)
}
// SwarmGetUnlockKey mocks base method.
func (m *MockAPIClient) SwarmGetUnlockKey(arg0 context.Context) (client.SwarmGetUnlockKeyResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmGetUnlockKey", arg0)
ret0, _ := ret[0].(client.SwarmGetUnlockKeyResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmGetUnlockKey indicates an expected call of SwarmGetUnlockKey.
func (mr *MockAPIClientMockRecorder) SwarmGetUnlockKey(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmGetUnlockKey", reflect.TypeOf((*MockAPIClient)(nil).SwarmGetUnlockKey), arg0)
}
// SwarmInit mocks base method.
func (m *MockAPIClient) SwarmInit(arg0 context.Context, arg1 client.SwarmInitOptions) (client.SwarmInitResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmInit", arg0, arg1)
ret0, _ := ret[0].(client.SwarmInitResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmInit indicates an expected call of SwarmInit.
func (mr *MockAPIClientMockRecorder) SwarmInit(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmInit", reflect.TypeOf((*MockAPIClient)(nil).SwarmInit), arg0, arg1)
}
// SwarmInspect mocks base method.
func (m *MockAPIClient) SwarmInspect(arg0 context.Context, arg1 client.SwarmInspectOptions) (client.SwarmInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmInspect", arg0, arg1)
ret0, _ := ret[0].(client.SwarmInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmInspect indicates an expected call of SwarmInspect.
func (mr *MockAPIClientMockRecorder) SwarmInspect(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmInspect", reflect.TypeOf((*MockAPIClient)(nil).SwarmInspect), arg0, arg1)
}
// SwarmJoin mocks base method.
func (m *MockAPIClient) SwarmJoin(arg0 context.Context, arg1 client.SwarmJoinOptions) (client.SwarmJoinResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmJoin", arg0, arg1)
ret0, _ := ret[0].(client.SwarmJoinResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmJoin indicates an expected call of SwarmJoin.
func (mr *MockAPIClientMockRecorder) SwarmJoin(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmJoin", reflect.TypeOf((*MockAPIClient)(nil).SwarmJoin), arg0, arg1)
}
// SwarmLeave mocks base method.
func (m *MockAPIClient) SwarmLeave(arg0 context.Context, arg1 client.SwarmLeaveOptions) (client.SwarmLeaveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmLeave", arg0, arg1)
ret0, _ := ret[0].(client.SwarmLeaveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmLeave indicates an expected call of SwarmLeave.
func (mr *MockAPIClientMockRecorder) SwarmLeave(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmLeave", reflect.TypeOf((*MockAPIClient)(nil).SwarmLeave), arg0, arg1)
}
// SwarmUnlock mocks base method.
func (m *MockAPIClient) SwarmUnlock(arg0 context.Context, arg1 client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmUnlock", arg0, arg1)
ret0, _ := ret[0].(client.SwarmUnlockResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmUnlock indicates an expected call of SwarmUnlock.
func (mr *MockAPIClientMockRecorder) SwarmUnlock(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmUnlock", reflect.TypeOf((*MockAPIClient)(nil).SwarmUnlock), arg0, arg1)
}
// SwarmUpdate mocks base method.
func (m *MockAPIClient) SwarmUpdate(arg0 context.Context, arg1 client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwarmUpdate", arg0, arg1)
ret0, _ := ret[0].(client.SwarmUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SwarmUpdate indicates an expected call of SwarmUpdate.
func (mr *MockAPIClientMockRecorder) SwarmUpdate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmUpdate", reflect.TypeOf((*MockAPIClient)(nil).SwarmUpdate), arg0, arg1)
}
// TaskInspect mocks base method.
func (m *MockAPIClient) TaskInspect(arg0 context.Context, arg1 string, arg2 client.TaskInspectOptions) (client.TaskInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.TaskInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TaskInspect indicates an expected call of TaskInspect.
func (mr *MockAPIClientMockRecorder) TaskInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskInspect", reflect.TypeOf((*MockAPIClient)(nil).TaskInspect), arg0, arg1, arg2)
}
// TaskList mocks base method.
func (m *MockAPIClient) TaskList(arg0 context.Context, arg1 client.TaskListOptions) (client.TaskListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskList", arg0, arg1)
ret0, _ := ret[0].(client.TaskListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TaskList indicates an expected call of TaskList.
func (mr *MockAPIClientMockRecorder) TaskList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskList", reflect.TypeOf((*MockAPIClient)(nil).TaskList), arg0, arg1)
}
// TaskLogs mocks base method.
func (m *MockAPIClient) TaskLogs(arg0 context.Context, arg1 string, arg2 client.TaskLogsOptions) (client.TaskLogsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskLogs", arg0, arg1, arg2)
ret0, _ := ret[0].(client.TaskLogsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TaskLogs indicates an expected call of TaskLogs.
func (mr *MockAPIClientMockRecorder) TaskLogs(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskLogs", reflect.TypeOf((*MockAPIClient)(nil).TaskLogs), arg0, arg1, arg2)
}
// VolumeCreate mocks base method.
func (m *MockAPIClient) VolumeCreate(arg0 context.Context, arg1 client.VolumeCreateOptions) (client.VolumeCreateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeCreate", arg0, arg1)
ret0, _ := ret[0].(client.VolumeCreateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumeCreate indicates an expected call of VolumeCreate.
func (mr *MockAPIClientMockRecorder) VolumeCreate(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockAPIClient)(nil).VolumeCreate), arg0, arg1)
}
// VolumeInspect mocks base method.
func (m *MockAPIClient) VolumeInspect(arg0 context.Context, arg1 string, arg2 client.VolumeInspectOptions) (client.VolumeInspectResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeInspect", arg0, arg1, arg2)
ret0, _ := ret[0].(client.VolumeInspectResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumeInspect indicates an expected call of VolumeInspect.
func (mr *MockAPIClientMockRecorder) VolumeInspect(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeInspect", reflect.TypeOf((*MockAPIClient)(nil).VolumeInspect), arg0, arg1, arg2)
}
// VolumeList mocks base method.
func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 client.VolumeListOptions) (client.VolumeListResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeList", arg0, arg1)
ret0, _ := ret[0].(client.VolumeListResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumeList indicates an expected call of VolumeList.
func (mr *MockAPIClientMockRecorder) VolumeList(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeList", reflect.TypeOf((*MockAPIClient)(nil).VolumeList), arg0, arg1)
}
// VolumePrune mocks base method.
func (m *MockAPIClient) VolumePrune(arg0 context.Context, arg1 client.VolumePruneOptions) (client.VolumePruneResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumePrune", arg0, arg1)
ret0, _ := ret[0].(client.VolumePruneResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumePrune indicates an expected call of VolumePrune.
func (mr *MockAPIClientMockRecorder) VolumePrune(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumePrune", reflect.TypeOf((*MockAPIClient)(nil).VolumePrune), arg0, arg1)
}
// VolumeRemove mocks base method.
func (m *MockAPIClient) VolumeRemove(arg0 context.Context, arg1 string, arg2 client.VolumeRemoveOptions) (client.VolumeRemoveResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeRemove", arg0, arg1, arg2)
ret0, _ := ret[0].(client.VolumeRemoveResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumeRemove indicates an expected call of VolumeRemove.
func (mr *MockAPIClientMockRecorder) VolumeRemove(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeRemove", reflect.TypeOf((*MockAPIClient)(nil).VolumeRemove), arg0, arg1, arg2)
}
// VolumeUpdate mocks base method.
func (m *MockAPIClient) VolumeUpdate(arg0 context.Context, arg1 string, arg2 client.VolumeUpdateOptions) (client.VolumeUpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeUpdate", arg0, arg1, arg2)
ret0, _ := ret[0].(client.VolumeUpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VolumeUpdate indicates an expected call of VolumeUpdate.
func (mr *MockAPIClientMockRecorder) VolumeUpdate(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeUpdate", reflect.TypeOf((*MockAPIClient)(nil).VolumeUpdate), arg0, arg1, arg2)
}
================================================
FILE: pkg/mocks/mock_docker_cli.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/docker/cli/cli/command (interfaces: Cli)
//
// Generated by this command:
//
// mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
//
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
command "github.com/docker/cli/cli/command"
configfile "github.com/docker/cli/cli/config/configfile"
docker "github.com/docker/cli/cli/context/docker"
store "github.com/docker/cli/cli/context/store"
streams "github.com/docker/cli/cli/streams"
client "github.com/moby/moby/client"
metric "go.opentelemetry.io/otel/metric"
resource "go.opentelemetry.io/otel/sdk/resource"
trace "go.opentelemetry.io/otel/trace"
gomock "go.uber.org/mock/gomock"
)
// MockCli is a mock of Cli interface.
type MockCli struct {
ctrl *gomock.Controller
recorder *MockCliMockRecorder
}
// MockCliMockRecorder is the mock recorder for MockCli.
type MockCliMockRecorder struct {
mock *MockCli
}
// NewMockCli creates a new mock instance.
func NewMockCli(ctrl *gomock.Controller) *MockCli {
mock := &MockCli{ctrl: ctrl}
mock.recorder = &MockCliMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCli) EXPECT() *MockCliMockRecorder {
return m.recorder
}
// BuildKitEnabled mocks base method.
func (m *MockCli) BuildKitEnabled() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildKitEnabled")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildKitEnabled indicates an expected call of BuildKitEnabled.
func (mr *MockCliMockRecorder) BuildKitEnabled() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildKitEnabled", reflect.TypeOf((*MockCli)(nil).BuildKitEnabled))
}
// Client mocks base method.
func (m *MockCli) Client() client.APIClient {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client")
ret0, _ := ret[0].(client.APIClient)
return ret0
}
// Client indicates an expected call of Client.
func (mr *MockCliMockRecorder) Client() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockCli)(nil).Client))
}
// ConfigFile mocks base method.
func (m *MockCli) ConfigFile() *configfile.ConfigFile {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigFile")
ret0, _ := ret[0].(*configfile.ConfigFile)
return ret0
}
// ConfigFile indicates an expected call of ConfigFile.
func (mr *MockCliMockRecorder) ConfigFile() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigFile", reflect.TypeOf((*MockCli)(nil).ConfigFile))
}
// ContextStore mocks base method.
func (m *MockCli) ContextStore() store.Store {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContextStore")
ret0, _ := ret[0].(store.Store)
return ret0
}
// ContextStore indicates an expected call of ContextStore.
func (mr *MockCliMockRecorder) ContextStore() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextStore", reflect.TypeOf((*MockCli)(nil).ContextStore))
}
// CurrentContext mocks base method.
func (m *MockCli) CurrentContext() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentContext")
ret0, _ := ret[0].(string)
return ret0
}
// CurrentContext indicates an expected call of CurrentContext.
func (mr *MockCliMockRecorder) CurrentContext() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockCli)(nil).CurrentContext))
}
// CurrentVersion mocks base method.
func (m *MockCli) CurrentVersion() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentVersion")
ret0, _ := ret[0].(string)
return ret0
}
// CurrentVersion indicates an expected call of CurrentVersion.
func (mr *MockCliMockRecorder) CurrentVersion() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentVersion", reflect.TypeOf((*MockCli)(nil).CurrentVersion))
}
// DockerEndpoint mocks base method.
func (m *MockCli) DockerEndpoint() docker.Endpoint {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DockerEndpoint")
ret0, _ := ret[0].(docker.Endpoint)
return ret0
}
// DockerEndpoint indicates an expected call of DockerEndpoint.
func (mr *MockCliMockRecorder) DockerEndpoint() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DockerEndpoint", reflect.TypeOf((*MockCli)(nil).DockerEndpoint))
}
// Err mocks base method.
func (m *MockCli) Err() *streams.Out {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(*streams.Out)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockCliMockRecorder) Err() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockCli)(nil).Err))
}
// In mocks base method.
func (m *MockCli) In() *streams.In {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "In")
ret0, _ := ret[0].(*streams.In)
return ret0
}
// In indicates an expected call of In.
func (mr *MockCliMockRecorder) In() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "In", reflect.TypeOf((*MockCli)(nil).In))
}
// MeterProvider mocks base method.
func (m *MockCli) MeterProvider() metric.MeterProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MeterProvider")
ret0, _ := ret[0].(metric.MeterProvider)
return ret0
}
// MeterProvider indicates an expected call of MeterProvider.
func (mr *MockCliMockRecorder) MeterProvider() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MeterProvider", reflect.TypeOf((*MockCli)(nil).MeterProvider))
}
// Out mocks base method.
func (m *MockCli) Out() *streams.Out {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Out")
ret0, _ := ret[0].(*streams.Out)
return ret0
}
// Out indicates an expected call of Out.
func (mr *MockCliMockRecorder) Out() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Out", reflect.TypeOf((*MockCli)(nil).Out))
}
// Resource mocks base method.
func (m *MockCli) Resource() *resource.Resource {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Resource")
ret0, _ := ret[0].(*resource.Resource)
return ret0
}
// Resource indicates an expected call of Resource.
func (mr *MockCliMockRecorder) Resource() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resource", reflect.TypeOf((*MockCli)(nil).Resource))
}
// ServerInfo mocks base method.
func (m *MockCli) ServerInfo() command.ServerInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerInfo")
ret0, _ := ret[0].(command.ServerInfo)
return ret0
}
// ServerInfo indicates an expected call of ServerInfo.
func (mr *MockCliMockRecorder) ServerInfo() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerInfo", reflect.TypeOf((*MockCli)(nil).ServerInfo))
}
// SetIn mocks base method.
func (m *MockCli) SetIn(arg0 *streams.In) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetIn", arg0)
}
// SetIn indicates an expected call of SetIn.
func (mr *MockCliMockRecorder) SetIn(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIn", reflect.TypeOf((*MockCli)(nil).SetIn), arg0)
}
// TracerProvider mocks base method.
func (m *MockCli) TracerProvider() trace.TracerProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TracerProvider")
ret0, _ := ret[0].(trace.TracerProvider)
return ret0
}
// TracerProvider indicates an expected call of TracerProvider.
func (mr *MockCliMockRecorder) TracerProvider() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TracerProvider", reflect.TypeOf((*MockCli)(nil).TracerProvider))
}
================================================
FILE: pkg/mocks/mock_docker_compose_api.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ./pkg/api/api.go
//
// Generated by this command:
//
// mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
types "github.com/compose-spec/compose-go/v2/types"
api "github.com/docker/compose/v5/pkg/api"
gomock "go.uber.org/mock/gomock"
)
// MockCompose is a mock of Compose interface.
type MockCompose struct {
ctrl *gomock.Controller
recorder *MockComposeMockRecorder
}
// MockComposeMockRecorder is the mock recorder for MockCompose.
type MockComposeMockRecorder struct {
mock *MockCompose
}
// NewMockCompose creates a new mock instance.
func NewMockCompose(ctrl *gomock.Controller) *MockCompose {
mock := &MockCompose{ctrl: ctrl}
mock.recorder = &MockComposeMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCompose) EXPECT() *MockComposeMockRecorder {
return m.recorder
}
// Attach mocks base method.
func (m *MockCompose) Attach(ctx context.Context, projectName string, options api.AttachOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Attach", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Attach indicates an expected call of Attach.
func (mr *MockComposeMockRecorder) Attach(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attach", reflect.TypeOf((*MockCompose)(nil).Attach), ctx, projectName, options)
}
// Build mocks base method.
func (m *MockCompose) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Build", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Build indicates an expected call of Build.
func (mr *MockComposeMockRecorder) Build(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockCompose)(nil).Build), ctx, project, options)
}
// Commit mocks base method.
func (m *MockCompose) Commit(ctx context.Context, projectName string, options api.CommitOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Commit", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Commit indicates an expected call of Commit.
func (mr *MockComposeMockRecorder) Commit(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockCompose)(nil).Commit), ctx, projectName, options)
}
// Copy mocks base method.
func (m *MockCompose) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Copy", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Copy indicates an expected call of Copy.
func (mr *MockComposeMockRecorder) Copy(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockCompose)(nil).Copy), ctx, projectName, options)
}
// Create mocks base method.
func (m *MockCompose) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockComposeMockRecorder) Create(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCompose)(nil).Create), ctx, project, options)
}
// Down mocks base method.
func (m *MockCompose) Down(ctx context.Context, projectName string, options api.DownOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Down", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Down indicates an expected call of Down.
func (mr *MockComposeMockRecorder) Down(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Down", reflect.TypeOf((*MockCompose)(nil).Down), ctx, projectName, options)
}
// Events mocks base method.
func (m *MockCompose) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Events", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Events indicates an expected call of Events.
func (mr *MockComposeMockRecorder) Events(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockCompose)(nil).Events), ctx, projectName, options)
}
// Exec mocks base method.
func (m *MockCompose) Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exec", ctx, projectName, options)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec.
func (mr *MockComposeMockRecorder) Exec(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCompose)(nil).Exec), ctx, projectName, options)
}
// Export mocks base method.
func (m *MockCompose) 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 *MockComposeMockRecorder) Export(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockCompose)(nil).Export), ctx, projectName, options)
}
// Generate mocks base method.
func (m *MockCompose) 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 *MockComposeMockRecorder) Generate(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockCompose)(nil).Generate), ctx, options)
}
// Images mocks base method.
func (m *MockCompose) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Images", ctx, projectName, options)
ret0, _ := ret[0].(map[string]api.ImageSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Images indicates an expected call of Images.
func (mr *MockComposeMockRecorder) Images(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Images", reflect.TypeOf((*MockCompose)(nil).Images), ctx, projectName, options)
}
// Kill mocks base method.
func (m *MockCompose) Kill(ctx context.Context, projectName string, options api.KillOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Kill", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Kill indicates an expected call of Kill.
func (mr *MockComposeMockRecorder) Kill(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kill", reflect.TypeOf((*MockCompose)(nil).Kill), ctx, projectName, options)
}
// List mocks base method.
func (m *MockCompose) List(ctx context.Context, options api.ListOptions) ([]api.Stack, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].([]api.Stack)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockComposeMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCompose)(nil).List), ctx, options)
}
// LoadProject mocks base method.
func (m *MockCompose) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadProject", ctx, options)
ret0, _ := ret[0].(*types.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadProject indicates an expected call of LoadProject.
func (mr *MockComposeMockRecorder) LoadProject(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadProject", reflect.TypeOf((*MockCompose)(nil).LoadProject), ctx, options)
}
// Logs mocks base method.
func (m *MockCompose) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, projectName, consumer, options)
ret0, _ := ret[0].(error)
return ret0
}
// Logs indicates an expected call of Logs.
func (mr *MockComposeMockRecorder) Logs(ctx, projectName, consumer, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockCompose)(nil).Logs), ctx, projectName, consumer, options)
}
// Pause mocks base method.
func (m *MockCompose) Pause(ctx context.Context, projectName string, options api.PauseOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Pause", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Pause indicates an expected call of Pause.
func (mr *MockComposeMockRecorder) Pause(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockCompose)(nil).Pause), ctx, projectName, options)
}
// Port mocks base method.
func (m *MockCompose) Port(ctx context.Context, projectName, service string, port uint16, options api.PortOptions) (string, int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Port", ctx, projectName, service, port, options)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(int)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Port indicates an expected call of Port.
func (mr *MockComposeMockRecorder) Port(ctx, projectName, service, port, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Port", reflect.TypeOf((*MockCompose)(nil).Port), ctx, projectName, service, port, options)
}
// Ps mocks base method.
func (m *MockCompose) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ps", ctx, projectName, options)
ret0, _ := ret[0].([]api.ContainerSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ps indicates an expected call of Ps.
func (mr *MockComposeMockRecorder) Ps(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ps", reflect.TypeOf((*MockCompose)(nil).Ps), ctx, projectName, options)
}
// Publish mocks base method.
func (m *MockCompose) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, project, repository, options)
ret0, _ := ret[0].(error)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockComposeMockRecorder) Publish(ctx, project, repository, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockCompose)(nil).Publish), ctx, project, repository, options)
}
// Pull mocks base method.
func (m *MockCompose) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Pull", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Pull indicates an expected call of Pull.
func (mr *MockComposeMockRecorder) Pull(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pull", reflect.TypeOf((*MockCompose)(nil).Pull), ctx, project, options)
}
// Push mocks base method.
func (m *MockCompose) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Push", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Push indicates an expected call of Push.
func (mr *MockComposeMockRecorder) Push(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockCompose)(nil).Push), ctx, project, options)
}
// Remove mocks base method.
func (m *MockCompose) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockComposeMockRecorder) Remove(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCompose)(nil).Remove), ctx, projectName, options)
}
// Restart mocks base method.
func (m *MockCompose) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Restart", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Restart indicates an expected call of Restart.
func (mr *MockComposeMockRecorder) Restart(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restart", reflect.TypeOf((*MockCompose)(nil).Restart), ctx, projectName, options)
}
// RunOneOffContainer mocks base method.
func (m *MockCompose) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunOneOffContainer", ctx, project, opts)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunOneOffContainer indicates an expected call of RunOneOffContainer.
func (mr *MockComposeMockRecorder) RunOneOffContainer(ctx, project, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOneOffContainer", reflect.TypeOf((*MockCompose)(nil).RunOneOffContainer), ctx, project, opts)
}
// Scale mocks base method.
func (m *MockCompose) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Scale", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Scale indicates an expected call of Scale.
func (mr *MockComposeMockRecorder) Scale(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scale", reflect.TypeOf((*MockCompose)(nil).Scale), ctx, project, options)
}
// Start mocks base method.
func (m *MockCompose) Start(ctx context.Context, projectName string, options api.StartOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Start", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Start indicates an expected call of Start.
func (mr *MockComposeMockRecorder) Start(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockCompose)(nil).Start), ctx, projectName, options)
}
// Stop mocks base method.
func (m *MockCompose) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stop", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Stop indicates an expected call of Stop.
func (mr *MockComposeMockRecorder) Stop(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockCompose)(nil).Stop), ctx, projectName, options)
}
// Top mocks base method.
func (m *MockCompose) Top(ctx context.Context, projectName string, services []string) ([]api.ContainerProcSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Top", ctx, projectName, services)
ret0, _ := ret[0].([]api.ContainerProcSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Top indicates an expected call of Top.
func (mr *MockComposeMockRecorder) Top(ctx, projectName, services any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Top", reflect.TypeOf((*MockCompose)(nil).Top), ctx, projectName, services)
}
// UnPause mocks base method.
func (m *MockCompose) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnPause", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// UnPause indicates an expected call of UnPause.
func (mr *MockComposeMockRecorder) UnPause(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnPause", reflect.TypeOf((*MockCompose)(nil).UnPause), ctx, projectName, options)
}
// Up mocks base method.
func (m *MockCompose) Up(ctx context.Context, project *types.Project, options api.UpOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Up", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Up indicates an expected call of Up.
func (mr *MockComposeMockRecorder) Up(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*MockCompose)(nil).Up), ctx, project, options)
}
// Viz mocks base method.
func (m *MockCompose) Viz(ctx context.Context, project *types.Project, options api.VizOptions) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Viz", ctx, project, options)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Viz indicates an expected call of Viz.
func (mr *MockComposeMockRecorder) Viz(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockCompose)(nil).Viz), ctx, project, options)
}
// Volumes mocks base method.
func (m *MockCompose) Volumes(ctx context.Context, project string, options api.VolumesOptions) ([]api.VolumesSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Volumes", ctx, project, options)
ret0, _ := ret[0].([]api.VolumesSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Volumes indicates an expected call of Volumes.
func (mr *MockComposeMockRecorder) Volumes(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Volumes", reflect.TypeOf((*MockCompose)(nil).Volumes), ctx, project, options)
}
// Wait mocks base method.
func (m *MockCompose) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait", ctx, projectName, options)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Wait indicates an expected call of Wait.
func (mr *MockComposeMockRecorder) Wait(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockCompose)(nil).Wait), ctx, projectName, options)
}
// Watch mocks base method.
func (m *MockCompose) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Watch", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Watch indicates an expected call of Watch.
func (mr *MockComposeMockRecorder) Watch(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockCompose)(nil).Watch), ctx, project, options)
}
// MockLogConsumer is a mock of LogConsumer interface.
type MockLogConsumer struct {
ctrl *gomock.Controller
recorder *MockLogConsumerMockRecorder
}
// MockLogConsumerMockRecorder is the mock recorder for MockLogConsumer.
type MockLogConsumerMockRecorder struct {
mock *MockLogConsumer
}
// NewMockLogConsumer creates a new mock instance.
func NewMockLogConsumer(ctrl *gomock.Controller) *MockLogConsumer {
mock := &MockLogConsumer{ctrl: ctrl}
mock.recorder = &MockLogConsumerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogConsumer) EXPECT() *MockLogConsumerMockRecorder {
return m.recorder
}
// Err mocks base method.
func (m *MockLogConsumer) Err(containerName, message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Err", containerName, message)
}
// Err indicates an expected call of Err.
func (mr *MockLogConsumerMockRecorder) Err(containerName, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockLogConsumer)(nil).Err), containerName, message)
}
// Log mocks base method.
func (m *MockLogConsumer) Log(containerName, message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Log", containerName, message)
}
// Log indicates an expected call of Log.
func (mr *MockLogConsumerMockRecorder) Log(containerName, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogConsumer)(nil).Log), containerName, message)
}
// Status mocks base method.
func (m *MockLogConsumer) Status(container, msg string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Status", container, msg)
}
// Status indicates an expected call of Status.
func (mr *MockLogConsumerMockRecorder) Status(container, msg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockLogConsumer)(nil).Status), container, msg)
}
================================================
FILE: pkg/remote/cache.go
================================================
/*
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 remote
import (
"os"
"path/filepath"
)
func cacheDir() (string, error) {
cache, ok := os.LookupEnv("XDG_CACHE_HOME")
if ok {
return filepath.Join(cache, "docker-compose"), nil
}
path, err := osDependentCacheDir()
if err != nil {
return "", err
}
err = os.MkdirAll(path, 0o700)
return path, err
}
================================================
FILE: pkg/remote/cache_darwin.go
================================================
/*
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 remote
import (
"os"
"path/filepath"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentCacheDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "Library", "Caches", "docker-compose"), nil
}
================================================
FILE: pkg/remote/cache_unix.go
================================================
//go:build linux || openbsd || freebsd
/*
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 remote
import (
"os"
"path/filepath"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentCacheDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".cache", "docker-compose"), nil
}
================================================
FILE: pkg/remote/cache_windows.go
================================================
/*
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 remote
import (
"os"
"path/filepath"
"golang.org/x/sys/windows"
)
// Based on https://github.com/adrg/xdg
// Licensed under MIT License (MIT)
// Copyright (c) 2014 Adrian-George Bostan
func osDependentCacheDir() (string, error) {
flags := []uint32{windows.KF_FLAG_DEFAULT, windows.KF_FLAG_DEFAULT_PATH}
for _, flag := range flags {
p, _ := windows.KnownFolderPath(windows.FOLDERID_LocalAppData, flag|windows.KF_FLAG_DONT_VERIFY)
if p != "" {
return filepath.Join(p, "cache", "docker-compose"), nil
}
}
appData, ok := os.LookupEnv("LOCALAPPDATA")
if ok {
return filepath.Join(appData, "cache", "docker-compose"), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "AppData", "Local", "cache", "docker-compose"), nil
}
================================================
FILE: pkg/remote/git.go
================================================
/*
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 remote
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/api"
)
const GIT_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_GIT_REMOTE"
func gitRemoteLoaderEnabled() (bool, error) {
if v := os.Getenv(GIT_REMOTE_ENABLED); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("COMPOSE_EXPERIMENTAL_GIT_REMOTE environment variable expects boolean value: %w", err)
}
return enabled, err
}
return true, nil
}
func NewGitRemoteLoader(dockerCli command.Cli, offline bool) loader.ResourceLoader {
return gitRemoteLoader{
dockerCli: dockerCli,
offline: offline,
known: map[string]string{},
}
}
type gitRemoteLoader struct {
dockerCli command.Cli
offline bool
known map[string]string
}
func (g gitRemoteLoader) Accept(path string) bool {
_, _, err := gitutil.ParseGitRef(path)
return err == nil
}
var commitSHA = regexp.MustCompile(`^[a-f0-9]{40}$`)
func (g gitRemoteLoader) Load(ctx context.Context, path string) (string, error) {
enabled, err := gitRemoteLoaderEnabled()
if err != nil {
return "", err
}
if !enabled {
return "", fmt.Errorf("git remote resource is disabled by %q", GIT_REMOTE_ENABLED)
}
ref, _, err := gitutil.ParseGitRef(path)
if err != nil {
return "", err
}
local, ok := g.known[path]
if !ok {
if ref.Ref == "" {
ref.Ref = "HEAD" // default branch
}
err = g.resolveGitRef(ctx, path, ref)
if err != nil {
return "", err
}
cache, err := cacheDir()
if err != nil {
return "", fmt.Errorf("initializing remote resource cache: %w", err)
}
local = filepath.Join(cache, ref.Ref)
if _, err := os.Stat(local); os.IsNotExist(err) {
if g.offline {
return "", nil
}
err = g.checkout(ctx, local, ref)
if err != nil {
return "", err
}
}
g.known[path] = local
}
if ref.SubDir != "" {
if err := validateGitSubDir(local, ref.SubDir); err != nil {
return "", err
}
local = filepath.Join(local, ref.SubDir)
}
stat, err := os.Stat(local)
if err != nil {
return "", err
}
if stat.IsDir() {
local, err = findFile(cli.DefaultFileNames, local)
}
return local, err
}
func (g gitRemoteLoader) Dir(path string) string {
return g.known[path]
}
// validateGitSubDir ensures a subdirectory path is contained within the base directory
// and doesn't escape via path traversal. Unlike validatePathInBase for OCI artifacts,
// this allows nested directories but prevents traversal outside the base.
func validateGitSubDir(base, subDir string) error {
cleanSubDir := filepath.Clean(subDir)
if filepath.IsAbs(cleanSubDir) {
return fmt.Errorf("git subdirectory must be relative, got: %s", subDir)
}
if cleanSubDir == ".." || strings.HasPrefix(cleanSubDir, "../") || strings.HasPrefix(cleanSubDir, "..\\") {
return fmt.Errorf("git subdirectory path traversal detected: %s", subDir)
}
if len(cleanSubDir) >= 2 && cleanSubDir[1] == ':' {
return fmt.Errorf("git subdirectory must be relative, got: %s", subDir)
}
targetPath := filepath.Join(base, cleanSubDir)
cleanBase := filepath.Clean(base)
cleanTarget := filepath.Clean(targetPath)
// Ensure the target starts with the base path
relPath, err := filepath.Rel(cleanBase, cleanTarget)
if err != nil {
return fmt.Errorf("invalid git subdirectory path: %w", err)
}
if relPath == ".." || strings.HasPrefix(relPath, "../") || strings.HasPrefix(relPath, "..\\") {
return fmt.Errorf("git subdirectory escapes base directory: %s", subDir)
}
return nil
}
func (g gitRemoteLoader) resolveGitRef(ctx context.Context, path string, ref *gitutil.GitRef) error {
if !commitSHA.MatchString(ref.Ref) {
cmd := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", ref.Remote, ref.Ref)
cmd.Env = g.gitCommandEnv()
out, err := cmd.CombinedOutput()
if err != nil {
if cmd.ProcessState.ExitCode() == 2 {
return fmt.Errorf("repository does not contain ref %s, output: %q: %w", path, string(out), err)
}
return fmt.Errorf("failed to access repository at %s:\n %s", ref.Remote, out)
}
if len(out) < 40 {
return fmt.Errorf("unexpected git command output: %q", string(out))
}
sha := string(out[:40])
if !commitSHA.MatchString(sha) {
return fmt.Errorf("invalid commit sha %q", sha)
}
ref.Ref = sha
}
return nil
}
func (g gitRemoteLoader) checkout(ctx context.Context, path string, ref *gitutil.GitRef) error {
err := os.MkdirAll(path, 0o700)
if err != nil {
return err
}
err = exec.CommandContext(ctx, "git", "init", path).Run()
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, "git", "remote", "add", "origin", ref.Remote)
cmd.Dir = path
err = cmd.Run()
if err != nil {
return err
}
cmd = exec.CommandContext(ctx, "git", "fetch", "--depth=1", "origin", ref.Ref)
cmd.Env = g.gitCommandEnv()
cmd.Dir = path
err = g.run(cmd)
if err != nil {
return err
}
cmd = exec.CommandContext(ctx, "git", "checkout", ref.Ref)
cmd.Dir = path
err = cmd.Run()
if err != nil {
return err
}
return nil
}
func (g gitRemoteLoader) run(cmd *exec.Cmd) error {
if logrus.IsLevelEnabled(logrus.DebugLevel) {
output, err := cmd.CombinedOutput()
scanner := bufio.NewScanner(bytes.NewBuffer(output))
for scanner.Scan() {
line := scanner.Text()
logrus.Debug(line)
}
return err
}
return cmd.Run()
}
func (g gitRemoteLoader) gitCommandEnv() []string {
env := types.NewMapping(os.Environ())
if env["GIT_TERMINAL_PROMPT"] == "" {
// Disable prompting for passwords by Git until user explicitly asks for it.
env["GIT_TERMINAL_PROMPT"] = "0"
}
if env["GIT_SSH"] == "" && env["GIT_SSH_COMMAND"] == "" {
// Disable any ssh connection pooling by Git and do not attempt to prompt the user.
env["GIT_SSH_COMMAND"] = "ssh -o ControlMaster=no -o BatchMode=yes"
}
v := env.Values()
return v
}
func findFile(names []string, pwd string) (string, error) {
for _, n := range names {
f := filepath.Join(pwd, n)
if fi, err := os.Stat(f); err == nil && !fi.IsDir() {
return f, nil
}
}
return "", api.ErrNotFound
}
var _ loader.ResourceLoader = gitRemoteLoader{}
================================================
FILE: pkg/remote/git_test.go
================================================
/*
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 remote
import (
"testing"
"gotest.tools/v3/assert"
)
func TestValidateGitSubDir(t *testing.T) {
base := "/tmp/cache/compose/abc123def456"
tests := []struct {
name string
subDir string
wantErr bool
}{
{
name: "valid simple directory",
subDir: "examples",
wantErr: false,
},
{
name: "valid nested directory",
subDir: "examples/nginx",
wantErr: false,
},
{
name: "valid deeply nested directory",
subDir: "examples/web/frontend/config",
wantErr: false,
},
{
name: "valid current directory",
subDir: ".",
wantErr: false,
},
{
name: "valid directory with redundant separators",
subDir: "examples//nginx",
wantErr: false,
},
{
name: "valid directory with dots in name",
subDir: "examples/nginx.conf.d",
wantErr: false,
},
{
name: "path traversal - parent directory",
subDir: "..",
wantErr: true,
},
{
name: "path traversal - multiple parent directories",
subDir: "../../../etc/passwd",
wantErr: true,
},
{
name: "path traversal - deeply nested escape",
subDir: "../../../../../../../tmp/pwned",
wantErr: true,
},
{
name: "path traversal - mixed with valid path",
subDir: "examples/../../etc/passwd",
wantErr: true,
},
{
name: "path traversal - at the end",
subDir: "examples/..",
wantErr: false, // This resolves to "." which is the current directory, safe
},
{
name: "path traversal - in the middle",
subDir: "examples/../../../etc/passwd",
wantErr: true,
},
{
name: "path traversal - windows style",
subDir: "..\\..\\..\\windows\\system32",
wantErr: true,
},
{
name: "absolute unix path",
subDir: "/etc/passwd",
wantErr: true,
},
{
name: "absolute windows path",
subDir: "C:\\windows\\system32\\config\\sam",
wantErr: true,
},
{
name: "absolute path with home directory",
subDir: "/home/user/.ssh/id_rsa",
wantErr: true,
},
{
name: "normalized path that would escape",
subDir: "./../../etc/passwd",
wantErr: true,
},
{
name: "directory name with three dots",
subDir: ".../config",
wantErr: false,
},
{
name: "directory name with four dots",
subDir: "..../config",
wantErr: false,
},
{
name: "directory name with five dots",
subDir: "...../etc/passwd",
wantErr: false, // ".....'' is a valid directory name, not path traversal
},
{
name: "directory name starting with two dots and letter",
subDir: "..foo/bar",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitSubDir(base, tt.subDir)
if (err != nil) != tt.wantErr {
t.Errorf("validateGitSubDir(%q, %q) error = %v, wantErr %v",
base, tt.subDir, err, tt.wantErr)
}
})
}
}
// TestValidateGitSubDirSecurityScenarios tests specific security scenarios
func TestValidateGitSubDirSecurityScenarios(t *testing.T) {
base := "/var/cache/docker-compose/git/1234567890abcdef"
// Test the exact vulnerability scenario from the issue
t.Run("CVE scenario - /tmp traversal", func(t *testing.T) {
maliciousPath := "../../../../../../../tmp/pwned"
err := validateGitSubDir(base, maliciousPath)
assert.ErrorContains(t, err, "path traversal")
})
// Test variations of the attack
t.Run("CVE scenario - /etc traversal", func(t *testing.T) {
maliciousPath := "../../../../../../../../etc/passwd"
err := validateGitSubDir(base, maliciousPath)
assert.ErrorContains(t, err, "path traversal")
})
// Test that legitimate nested paths still work
t.Run("legitimate nested path", func(t *testing.T) {
validPath := "examples/docker-compose/nginx/config"
err := validateGitSubDir(base, validPath)
assert.NilError(t, err)
})
}
================================================
FILE: pkg/remote/oci.go
================================================
/*
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 remote
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
spec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/compose/v5/internal/oci"
"github.com/docker/compose/v5/pkg/api"
)
const (
OCI_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_OCI_REMOTE"
OciPrefix = "oci://"
)
// validatePathInBase ensures a file path is contained within the base directory,
// as OCI artifacts resources must all live within the same folder.
func validatePathInBase(base, unsafePath string) error {
// Reject paths with path separators regardless of OS
if strings.ContainsAny(unsafePath, "\\/") {
return fmt.Errorf("invalid OCI artifact")
}
// Join the base with the untrusted path
targetPath := filepath.Join(base, unsafePath)
// Get the directory of the target path
targetDir := filepath.Dir(targetPath)
// Clean both paths to resolve any .. or . components
cleanBase := filepath.Clean(base)
cleanTargetDir := filepath.Clean(targetDir)
// Check if the target directory is the same as base directory
if cleanTargetDir != cleanBase {
return fmt.Errorf("invalid OCI artifact")
}
return nil
}
func ociRemoteLoaderEnabled() (bool, error) {
if v := os.Getenv(OCI_REMOTE_ENABLED); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("COMPOSE_EXPERIMENTAL_OCI_REMOTE environment variable expects boolean value: %w", err)
}
return enabled, err
}
return true, nil
}
func NewOCIRemoteLoader(dockerCli command.Cli, offline bool, options api.OCIOptions) loader.ResourceLoader {
return ociRemoteLoader{
dockerCli: dockerCli,
offline: offline,
known: map[string]string{},
insecureRegistries: options.InsecureRegistries,
}
}
type ociRemoteLoader struct {
dockerCli command.Cli
offline bool
known map[string]string
insecureRegistries []string
}
func (g ociRemoteLoader) Accept(path string) bool {
return strings.HasPrefix(path, OciPrefix)
}
//nolint:gocyclo
func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
enabled, err := ociRemoteLoaderEnabled()
if err != nil {
return "", err
}
if !enabled {
return "", fmt.Errorf("OCI remote resource is disabled by %q", OCI_REMOTE_ENABLED)
}
if g.offline {
return "", nil
}
local, ok := g.known[path]
if !ok {
ref, err := reference.ParseDockerRef(path[len(OciPrefix):])
if err != nil {
return "", err
}
resolver := oci.NewResolver(g.dockerCli.ConfigFile(), g.insecureRegistries...)
descriptor, content, err := oci.Get(ctx, resolver, ref)
if err != nil {
return "", fmt.Errorf("failed to pull OCI resource %q: %w", ref, err)
}
cache, err := cacheDir()
if err != nil {
return "", fmt.Errorf("initializing remote resource cache: %w", err)
}
local = filepath.Join(cache, descriptor.Digest.Hex())
if _, err = os.Stat(local); os.IsNotExist(err) {
// a Compose application bundle is published as image index
if images.IsIndexType(descriptor.MediaType) {
var index spec.Index
err = json.Unmarshal(content, &index)
if err != nil {
return "", err
}
found := false
for _, manifest := range index.Manifests {
if manifest.ArtifactType != oci.ComposeProjectArtifactType {
continue
}
found = true
digested, err := reference.WithDigest(ref, manifest.Digest)
if err != nil {
return "", err
}
descriptor, content, err = oci.Get(ctx, resolver, digested)
if err != nil {
return "", fmt.Errorf("failed to pull OCI resource %q: %w", ref, err)
}
}
if !found {
return "", fmt.Errorf("OCI index %s doesn't refer to compose artifacts", ref)
}
}
var manifest spec.Manifest
err = json.Unmarshal(content, &manifest)
if err != nil {
return "", err
}
err = g.pullComposeFiles(ctx, local, manifest, ref, resolver)
if err != nil {
// we need to clean up the directory to be sure we won't let empty files present
_ = os.RemoveAll(local)
return "", err
}
}
g.known[path] = local
}
return filepath.Join(local, "compose.yaml"), nil
}
func (g ociRemoteLoader) Dir(path string) string {
return g.known[path]
}
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error {
err := os.MkdirAll(local, 0o700)
if err != nil {
return err
}
if (manifest.ArtifactType != "" && manifest.ArtifactType != oci.ComposeProjectArtifactType) ||
(manifest.ArtifactType == "" && manifest.Config.MediaType != oci.ComposeEmptyConfigMediaType) {
return fmt.Errorf("%s is not a compose project OCI artifact, but %s", ref.String(), manifest.ArtifactType)
}
for i, layer := range manifest.Layers {
digested, err := reference.WithDigest(ref, layer.Digest)
if err != nil {
return err
}
_, content, err := oci.Get(ctx, resolver, digested)
if err != nil {
return err
}
switch layer.MediaType {
case oci.ComposeYAMLMediaType:
if err := writeComposeFile(layer, i, local, content); err != nil {
return err
}
case oci.ComposeEnvFileMediaType:
if err := writeEnvFile(layer, local, content); err != nil {
return err
}
case oci.ComposeEmptyConfigMediaType:
}
}
return nil
}
func writeComposeFile(layer spec.Descriptor, i int, local string, content []byte) error {
file := "compose.yaml"
if _, ok := layer.Annotations["com.docker.compose.extends"]; ok {
file = layer.Annotations["com.docker.compose.file"]
if err := validatePathInBase(local, file); err != nil {
return err
}
}
f, err := os.OpenFile(filepath.Join(local, file), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
if _, ok := layer.Annotations["com.docker.compose.file"]; i > 0 && ok {
_, err := f.Write([]byte("\n---\n"))
if err != nil {
return err
}
}
_, err = f.Write(content)
return err
}
func writeEnvFile(layer spec.Descriptor, local string, content []byte) error {
envfilePath, ok := layer.Annotations["com.docker.compose.envfile"]
if !ok {
return fmt.Errorf("missing annotation com.docker.compose.envfile in layer %q", layer.Digest)
}
if err := validatePathInBase(local, envfilePath); err != nil {
return err
}
otherFile, err := os.Create(filepath.Join(local, envfilePath))
if err != nil {
return err
}
defer func() { _ = otherFile.Close() }()
_, err = otherFile.Write(content)
return err
}
var _ loader.ResourceLoader = ociRemoteLoader{}
================================================
FILE: pkg/remote/oci_test.go
================================================
/*
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 remote
import (
"path/filepath"
"testing"
spec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
)
func TestValidatePathInBase(t *testing.T) {
base := "/tmp/cache/compose"
tests := []struct {
name string
unsafePath string
wantErr bool
}{
{
name: "valid simple filename",
unsafePath: "compose.yaml",
wantErr: false,
},
{
name: "valid hashed filename",
unsafePath: "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
wantErr: false,
},
{
name: "valid env file",
unsafePath: ".env",
wantErr: false,
},
{
name: "valid env file with suffix",
unsafePath: ".env.prod",
wantErr: false,
},
{
name: "unix path traversal",
unsafePath: "../../../etc/passwd",
wantErr: true,
},
{
name: "windows path traversal",
unsafePath: "..\\..\\..\\windows\\system32\\config\\sam",
wantErr: true,
},
{
name: "subdirectory unix",
unsafePath: "config/base.yaml",
wantErr: true,
},
{
name: "subdirectory windows",
unsafePath: "config\\base.yaml",
wantErr: true,
},
{
name: "absolute unix path",
unsafePath: "/etc/passwd",
wantErr: true,
},
{
name: "absolute windows path",
unsafePath: "C:\\windows\\system32\\config\\sam",
wantErr: true,
},
{
name: "parent reference only",
unsafePath: "..",
wantErr: true,
},
{
name: "mixed separators",
unsafePath: "config/sub\\file.yaml",
wantErr: true,
},
{
name: "filename with spaces",
unsafePath: "my file.yaml",
wantErr: false,
},
{
name: "filename with special chars",
unsafePath: "file-name_v1.2.3.yaml",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePathInBase(base, tt.unsafePath)
if (err != nil) != tt.wantErr {
targetPath := filepath.Join(base, tt.unsafePath)
targetDir := filepath.Dir(targetPath)
t.Errorf("validatePathInBase(%q, %q) error = %v, wantErr %v\ntargetDir=%q base=%q",
base, tt.unsafePath, err, tt.wantErr, targetDir, base)
}
})
}
}
func TestWriteComposeFileWithExtendsPathTraversal(t *testing.T) {
tmpDir := t.TempDir()
// Create a layer with com.docker.compose.extends=true and a path traversal attempt
layer := spec.Descriptor{
MediaType: "application/vnd.docker.compose.file.v1+yaml",
Digest: "sha256:test123",
Size: 100,
Annotations: map[string]string{
"com.docker.compose.extends": "true",
"com.docker.compose.file": "../other",
},
}
content := []byte("services:\n test:\n image: nginx\n")
// writeComposeFile should return an error due to path traversal
err := writeComposeFile(layer, 0, tmpDir, content)
assert.Error(t, err, "invalid OCI artifact")
}
================================================
FILE: pkg/utils/durationutils.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import "time"
func DurationSecondToInt(d *time.Duration) *int {
if d == nil {
return nil
}
timeout := int(d.Seconds())
return &timeout
}
================================================
FILE: pkg/utils/safebuffer.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"bytes"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// SafeBuffer is a thread safe version of bytes.Buffer
type SafeBuffer struct {
m sync.RWMutex
b bytes.Buffer
}
// Read is a thread safe version of bytes.Buffer::Read
func (b *SafeBuffer) Read(p []byte) (n int, err error) {
b.m.RLock()
defer b.m.RUnlock()
return b.b.Read(p)
}
// Write is a thread safe version of bytes.Buffer::Write
func (b *SafeBuffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
// String is a thread safe version of bytes.Buffer::String
func (b *SafeBuffer) String() string {
b.m.RLock()
defer b.m.RUnlock()
return b.b.String()
}
// Bytes is a thread safe version of bytes.Buffer::Bytes
func (b *SafeBuffer) Bytes() []byte {
b.m.RLock()
defer b.m.RUnlock()
return b.b.Bytes()
}
// RequireEventuallyContains is a thread safe eventual checker for the buffer content
func (b *SafeBuffer) RequireEventuallyContains(t testing.TB, v string) {
t.Helper()
var bufContents strings.Builder
require.Eventuallyf(t, func() bool {
b.m.Lock()
defer b.m.Unlock()
if _, err := b.b.WriteTo(&bufContents); err != nil {
require.FailNowf(t, "Failed to copy from buffer",
"Error: %v", err)
}
return strings.Contains(bufContents.String(), v)
}, 2*time.Second, 20*time.Millisecond,
"Buffer did not contain %q\n============\n%s\n============",
v, &bufContents)
}
================================================
FILE: pkg/utils/set.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](v ...T) Set[T] {
if len(v) == 0 {
return make(Set[T])
}
out := make(Set[T], len(v))
for i := range v {
out.Add(v[i])
}
return out
}
func (s Set[T]) Has(v T) bool {
_, ok := s[v]
return ok
}
func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}
func (s Set[T]) AddAll(v ...T) {
for _, e := range v {
s[e] = struct{}{}
}
}
func (s Set[T]) Remove(v T) bool {
_, ok := s[v]
if ok {
delete(s, v)
}
return ok
}
func (s Set[T]) Clear() {
for v := range s {
delete(s, v)
}
}
func (s Set[T]) Elements() []T {
elements := make([]T, 0, len(s))
for v := range s {
elements = append(elements, v)
}
return elements
}
func (s Set[T]) RemoveAll(elements ...T) {
for _, e := range elements {
s.Remove(e)
}
}
func (s Set[T]) Diff(other Set[T]) Set[T] {
out := make(Set[T])
for k := range s {
if _, ok := other[k]; !ok {
out[k] = struct{}{}
}
}
return out
}
func (s Set[T]) Union(other Set[T]) Set[T] {
out := make(Set[T])
for k := range s {
out[k] = struct{}{}
}
for k := range other {
out[k] = struct{}{}
}
return out
}
================================================
FILE: pkg/utils/set_test.go
================================================
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSet_Has(t *testing.T) {
x := NewSet[string]("value")
require.True(t, x.Has("value"))
require.False(t, x.Has("VALUE"))
}
func TestSet_Diff(t *testing.T) {
a := NewSet[int](1, 2)
b := NewSet[int](2, 3)
require.ElementsMatch(t, []int{1}, a.Diff(b).Elements())
require.ElementsMatch(t, []int{3}, b.Diff(a).Elements())
}
func TestSet_Union(t *testing.T) {
a := NewSet[int](1, 2)
b := NewSet[int](2, 3)
require.ElementsMatch(t, []int{1, 2, 3}, a.Union(b).Elements())
require.ElementsMatch(t, []int{1, 2, 3}, b.Union(a).Elements())
}
================================================
FILE: pkg/utils/stringutils.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"strconv"
"strings"
)
// StringToBool converts a string to a boolean ignoring errors
func StringToBool(s string) bool {
s = strings.ToLower(strings.TrimSpace(s))
if s == "y" {
return true
}
b, _ := strconv.ParseBool(s)
return b
}
================================================
FILE: pkg/utils/writer.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"bytes"
"io"
)
// GetWriter creates an io.Writer that will actually split by line and format by LogConsumer
func GetWriter(consumer func(string)) io.WriteCloser {
return &splitWriter{
buffer: bytes.Buffer{},
consumer: consumer,
}
}
type splitWriter struct {
buffer bytes.Buffer
consumer func(string)
}
// Write implements io.Writer. joins all input, splits on the separator and yields each chunk
func (s *splitWriter) Write(b []byte) (int, error) {
n, err := s.buffer.Write(b)
if err != nil {
return n, err
}
for {
b = s.buffer.Bytes()
index := bytes.Index(b, []byte{'\n'})
if index < 0 {
break
}
line := s.buffer.Next(index + 1)
s.consumer(string(line[:len(line)-1]))
}
return n, nil
}
func (s *splitWriter) Close() error {
b := s.buffer.Bytes()
if len(b) == 0 {
return nil
}
s.consumer(string(b))
return nil
}
================================================
FILE: pkg/utils/writer_test.go
================================================
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"testing"
"gotest.tools/v3/assert"
)
//nolint:errcheck
func TestSplitWriter(t *testing.T) {
var lines []string
w := GetWriter(func(line string) {
lines = append(lines, line)
})
w.Write([]byte("h"))
w.Write([]byte("e"))
w.Write([]byte("l"))
w.Write([]byte("l"))
w.Write([]byte("o"))
w.Write([]byte("\n"))
w.Write([]byte("world!\n"))
assert.DeepEqual(t, lines, []string{"hello", "world!"})
}
================================================
FILE: pkg/watch/debounce.go
================================================
/*
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 (
"context"
"time"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v5/pkg/utils"
)
const QuietPeriod = 500 * time.Millisecond
// BatchDebounceEvents groups identical file events within a sliding time window and writes the results to the returned
// channel.
//
// The returned channel is closed when the debouncer is stopped via context cancellation or by closing the input channel.
func BatchDebounceEvents(ctx context.Context, clock clockwork.Clock, input <-chan FileEvent) <-chan []FileEvent {
out := make(chan []FileEvent)
go func() {
defer close(out)
seen := utils.Set[FileEvent]{}
flushEvents := func() {
if len(seen) == 0 {
return
}
logrus.Debugf("flush: %d events %s", len(seen), seen)
events := make([]FileEvent, 0, len(seen))
for e := range seen {
events = append(events, e)
}
out <- events
seen = utils.Set[FileEvent]{}
}
t := clock.NewTicker(QuietPeriod)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.Chan():
flushEvents()
case e, ok := <-input:
if !ok {
// input channel was closed
flushEvents()
return
}
if _, ok := seen[e]; !ok {
seen.Add(e)
}
t.Reset(QuietPeriod)
}
}
}()
return out
}
================================================
FILE: pkg/watch/debounce_test.go
================================================
/*
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 (
"context"
"slices"
"testing"
"time"
"github.com/jonboulle/clockwork"
"gotest.tools/v3/assert"
)
func Test_BatchDebounceEvents(t *testing.T) {
ch := make(chan FileEvent)
clock := clockwork.NewFakeClock()
ctx, stop := context.WithCancel(t.Context())
t.Cleanup(stop)
eventBatchCh := BatchDebounceEvents(ctx, clock, ch)
for i := range 100 {
path := "/a"
if i%2 == 0 {
path = "/b"
}
ch <- FileEvent(path)
}
// we sent 100 events + the debouncer
err := clock.BlockUntilContext(ctx, 101)
assert.NilError(t, err)
clock.Advance(QuietPeriod)
select {
case batch := <-eventBatchCh:
slices.Sort(batch)
assert.Equal(t, len(batch), 2)
assert.Equal(t, batch[0], FileEvent("/a"))
assert.Equal(t, batch[1], FileEvent("/b"))
case <-time.After(50 * time.Millisecond):
t.Fatal("timed out waiting for events")
}
err = clock.BlockUntilContext(ctx, 1)
assert.NilError(t, err)
clock.Advance(QuietPeriod)
// there should only be a single batch
select {
case batch := <-eventBatchCh:
t.Fatalf("unexpected events: %v", batch)
case <-time.After(50 * time.Millisecond):
// channel is empty
}
}
================================================
FILE: pkg/watch/dockerignore.go
================================================
/*
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 (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/moby/patternmatcher"
"github.com/moby/patternmatcher/ignorefile"
"github.com/docker/compose/v5/internal/paths"
)
type dockerPathMatcher struct {
repoRoot string
matcher *patternmatcher.PatternMatcher
}
func (i dockerPathMatcher) Matches(f string) (bool, error) {
if !filepath.IsAbs(f) {
f = filepath.Join(i.repoRoot, f)
}
return i.matcher.MatchesOrParentMatches(f)
}
func (i dockerPathMatcher) MatchesEntireDir(f string) (bool, error) {
matches, err := i.Matches(f)
if !matches || err != nil {
return matches, err
}
// We match the dir, but we might exclude files underneath it.
if i.matcher.Exclusions() {
for _, pattern := range i.matcher.Patterns() {
if !pattern.Exclusion() {
continue
}
if paths.IsChild(f, pattern.String()) {
// Found an exclusion match -- we don't match this whole dir
return false, nil
}
}
return true, nil
}
return true, nil
}
func LoadDockerIgnore(build *types.BuildConfig) (PathMatcher, error) {
if build == nil {
return EmptyMatcher{}, nil
}
repoRoot := build.Context
absRoot, err := filepath.Abs(repoRoot)
if err != nil {
return nil, err
}
// 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
}
return NewDockerPatternMatcher(absRoot, patterns)
}
// Make all the patterns use absolute paths.
func absPatterns(absRoot string, patterns []string) []string {
absPatterns := make([]string, 0, len(patterns))
for _, p := range patterns {
// The pattern parsing here is loosely adapted from fileutils' NewPatternMatcher
p = strings.TrimSpace(p)
if p == "" {
continue
}
p = filepath.Clean(p)
pPath := p
isExclusion := false
if p[0] == '!' {
pPath = p[1:]
isExclusion = true
}
if !filepath.IsAbs(pPath) {
pPath = filepath.Join(absRoot, pPath)
}
absPattern := pPath
if isExclusion {
absPattern = fmt.Sprintf("!%s", pPath)
}
absPatterns = append(absPatterns, absPattern)
}
return absPatterns
}
func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMatcher, error) {
absRoot, err := filepath.Abs(repoRoot)
if err != nil {
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
}
return &dockerPathMatcher{
repoRoot: absRoot,
matcher: pm,
}, nil
}
func readDockerignorePatterns(r io.Reader) ([]string, error) {
patterns, err := ignorefile.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
}
return patterns, nil
}
func DockerIgnoreTesterFromContents(repoRoot string, contents string) (*dockerPathMatcher, error) {
patterns, err := ignorefile.ReadAll(strings.NewReader(contents))
if err != nil {
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
}
return NewDockerPatternMatcher(repoRoot, patterns)
}
================================================
FILE: pkg/watch/dockerignore_test.go
================================================
/*
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])
}
}
}
})
}
}
================================================
FILE: pkg/watch/ephemeral.go
================================================
/*
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
// EphemeralPathMatcher filters out spurious changes that we don't want to
// rebuild on, like IDE temp/lock files.
//
// This isn't an ideal solution. In an ideal world, the user would put
// everything to ignore in their tiltignore/dockerignore files. This is a
// stop-gap so they don't have a terrible experience if those files aren't
// there or aren't in the right places.
//
// NOTE: The underlying `patternmatcher` is NOT always Goroutine-safe, so
// this is not a singleton; we create an instance for each watcher currently.
func EphemeralPathMatcher() PathMatcher {
golandPatterns := []string{"**/*___jb_old___", "**/*___jb_tmp___", "**/.idea/**"}
emacsPatterns := []string{"**/.#*", "**/#*#"}
// if .swp is taken (presumably because multiple vims are running in that dir),
// vim will go with .swo, .swn, etc, and then even .svz, .svy!
// https://github.com/vim/vim/blob/ea781459b9617aa47335061fcc78403495260315/src/memline.c#L5076
// ignoring .sw? seems dangerous, since things like .swf or .swi exist, but ignoring the first few
// seems safe and should catch most cases
vimPatterns := []string{"**/4913", "**/*~", "**/.*.swp", "**/.*.swx", "**/.*.swo", "**/.*.swn"}
// kate (the default text editor for KDE) uses a file similar to Vim's .swp
// files, but it doesn't have the "incrementing" character problem mentioned
// above
katePatterns := []string{"**/.*.kate-swp"}
// go stdlib creates tmpfiles to determine umask for setting permissions
// during file creation; they are then immediately deleted
// https://github.com/golang/go/blob/0b5218cf4e3e5c17344ea113af346e8e0836f6c4/src/cmd/go/internal/work/exec.go#L1764
goPatterns := []string{"**/*-go-tmp-umask"}
var allPatterns []string
allPatterns = append(allPatterns, golandPatterns...)
allPatterns = append(allPatterns, emacsPatterns...)
allPatterns = append(allPatterns, vimPatterns...)
allPatterns = append(allPatterns, katePatterns...)
allPatterns = append(allPatterns, goPatterns...)
matcher, err := NewDockerPatternMatcher("/", allPatterns)
if err != nil {
panic(err)
}
return matcher
}
================================================
FILE: pkg/watch/ephemeral_test.go
================================================
/*
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 watch_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v5/pkg/watch"
)
func TestEphemeralPathMatcher(t *testing.T) {
ignored := []string{
".file.txt.swp",
"/path/file.txt~",
"/home/moby/proj/.idea/modules.xml",
".#file.txt",
"#file.txt#",
"/dir/.file.txt.kate-swp",
"/go/app/1234-go-tmp-umask",
}
matcher := watch.EphemeralPathMatcher()
for _, p := range ignored {
ok, err := matcher.Matches(p)
require.NoErrorf(t, err, "Matching %s", p)
assert.Truef(t, ok, "Path %s should have matched", p)
}
const includedPath = "normal.txt"
ok, err := matcher.Matches(includedPath)
require.NoErrorf(t, err, "Matching %s", includedPath)
assert.Falsef(t, ok, "Path %s should NOT have matched", includedPath)
}
================================================
FILE: pkg/watch/notify.go
================================================
/*
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 (
"expvar"
"fmt"
"os"
"path/filepath"
"strconv"
)
var numberOfWatches = expvar.NewInt("watch.naive.numberOfWatches")
type FileEvent string
func NewFileEvent(p string) FileEvent {
if !filepath.IsAbs(p) {
panic(fmt.Sprintf("NewFileEvent only accepts absolute paths. Actual: %s", p))
}
return FileEvent(p)
}
type Notify interface {
// Start watching the paths set at init time
Start() error
// Stop watching and close all channels
Close() error
// A channel to read off incoming file changes
Events() chan FileEvent
// A channel to read off show-stopping errors
Errors() chan error
}
// When we specify directories to watch, we often want to
// ignore some subset of the files under those directories.
//
// For example:
// - Watch /src/repo, but ignore /src/repo/.git
// - Watch /src/repo, but ignore everything in /src/repo/bazel-bin except /src/repo/bazel-bin/app-binary
//
// The PathMatcher interface helps us manage these ignores.
type PathMatcher interface {
Matches(file string) (bool, error)
// If this matches the entire dir, we can often optimize filetree walks a bit.
MatchesEntireDir(file string) (bool, error)
}
// AnyMatcher is a PathMatcher to match any path
type AnyMatcher struct{}
func (AnyMatcher) Matches(f string) (bool, error) { return true, nil }
func (AnyMatcher) MatchesEntireDir(f string) (bool, error) { return true, nil }
var _ PathMatcher = AnyMatcher{}
// EmptyMatcher is a PathMatcher to match no path
type EmptyMatcher struct{}
func (EmptyMatcher) Matches(f string) (bool, error) { return false, nil }
func (EmptyMatcher) MatchesEntireDir(f string) (bool, error) { return false, nil }
var _ PathMatcher = EmptyMatcher{}
func NewWatcher(paths []string) (Notify, error) {
return newWatcher(paths)
}
const WindowsBufferSizeEnvVar = "COMPOSE_WATCH_WINDOWS_BUFFER_SIZE"
const defaultBufferSize int = 65536
func DesiredWindowsBufferSize() int {
envVar := os.Getenv(WindowsBufferSizeEnvVar)
if envVar != "" {
size, err := strconv.Atoi(envVar)
if err == nil {
return size
}
}
return defaultBufferSize
}
type CompositePathMatcher struct {
Matchers []PathMatcher
}
func NewCompositeMatcher(matchers ...PathMatcher) PathMatcher {
if len(matchers) == 0 {
return EmptyMatcher{}
}
return CompositePathMatcher{Matchers: matchers}
}
func (c CompositePathMatcher) Matches(f string) (bool, error) {
for _, t := range c.Matchers {
ret, err := t.Matches(f)
if err != nil {
return false, err
}
if ret {
return true, nil
}
}
return false, nil
}
func (c CompositePathMatcher) MatchesEntireDir(f string) (bool, error) {
for _, t := range c.Matchers {
matches, err := t.MatchesEntireDir(f)
if matches || err != nil {
return matches, err
}
}
return false, nil
}
var _ PathMatcher = CompositePathMatcher{}
================================================
FILE: pkg/watch/notify_test.go
================================================
/*
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 (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Each implementation of the notify interface should have the same basic
// behavior.
func TestWindowsBufferSize(t *testing.T) {
t.Run("empty value", func(t *testing.T) {
t.Setenv(WindowsBufferSizeEnvVar, "")
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
})
t.Run("invalid value", func(t *testing.T) {
t.Setenv(WindowsBufferSizeEnvVar, "a")
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
})
t.Run("valid value", func(t *testing.T) {
t.Setenv(WindowsBufferSizeEnvVar, "10")
assert.Equal(t, 10, DesiredWindowsBufferSize())
})
}
func TestNoEvents(t *testing.T) {
f := newNotifyFixture(t)
f.assertEvents()
}
func TestNoWatches(t *testing.T) {
f := newNotifyFixture(t)
f.paths = nil
f.rebuildWatcher()
f.assertEvents()
}
func TestEventOrdering(t *testing.T) {
if runtime.GOOS == "windows" {
// https://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw_19.html
t.Skip("Windows doesn't make great guarantees about duplicate/out-of-order events")
return
}
f := newNotifyFixture(t)
count := 8
dirs := make([]string, count)
for i := range dirs {
dir := f.TempDir("watched")
dirs[i] = dir
f.watch(dir)
}
f.fsync()
f.events = nil
var expected []string
for i, dir := range dirs {
base := fmt.Sprintf("%d.txt", i)
p := filepath.Join(dir, base)
err := os.WriteFile(p, []byte(base), os.FileMode(0o777))
if err != nil {
t.Fatal(err)
}
expected = append(expected, filepath.Join(dir, base))
}
f.assertEvents(expected...)
}
// Simulate a git branch switch that creates a bunch
// of directories, creates files in them, then deletes
// them all quickly. Make sure there are no errors.
func TestGitBranchSwitch(t *testing.T) {
f := newNotifyFixture(t)
count := 10
dirs := make([]string, count)
for i := range dirs {
dir := f.TempDir("watched")
dirs[i] = dir
f.watch(dir)
}
f.fsync()
f.events = nil
// consume all the events in the background
ctx, cancel := context.WithCancel(t.Context())
done := f.consumeEventsInBackground(ctx)
for i, dir := range dirs {
for j := range count {
base := fmt.Sprintf("x/y/dir-%d/x.txt", j)
p := filepath.Join(dir, base)
f.WriteFile(p, "contents")
}
if i != 0 {
err := os.RemoveAll(dir)
require.NoError(t, err)
}
}
cancel()
err := <-done
if err != nil {
t.Fatal(err)
}
f.fsync()
f.events = nil
// Make sure the watch on the first dir still works.
dir := dirs[0]
path := filepath.Join(dir, "change")
f.WriteFile(path, "hello\n")
f.fsync()
f.assertEvents(path)
// Make sure there are no errors in the out stream
assert.Empty(t, f.out.String())
}
func TestWatchesAreRecursive(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
// add a sub directory
subPath := filepath.Join(root, "sub")
f.MkdirAll(subPath)
// watch parent
f.watch(root)
f.fsync()
f.events = nil
// change sub directory
changeFilePath := filepath.Join(subPath, "change")
f.WriteFile(changeFilePath, "change")
f.assertEvents(changeFilePath)
}
func TestNewDirectoriesAreRecursivelyWatched(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
// watch parent
f.watch(root)
f.fsync()
f.events = nil
// add a sub directory
subPath := filepath.Join(root, "sub")
f.MkdirAll(subPath)
// change something inside sub directory
changeFilePath := filepath.Join(subPath, "change")
file, err := os.OpenFile(changeFilePath, os.O_RDONLY|os.O_CREATE, 0o666)
if err != nil {
t.Fatal(err)
}
_ = file.Close()
f.assertEvents(subPath, changeFilePath)
}
func TestWatchNonExistentPath(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
path := filepath.Join(root, "change")
f.watch(path)
f.fsync()
d1 := "hello\ngo\n"
f.WriteFile(path, d1)
f.assertEvents(path)
}
func TestWatchNonExistentPathDoesNotFireSiblingEvent(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
watchedFile := filepath.Join(root, "a.txt")
unwatchedSibling := filepath.Join(root, "b.txt")
f.watch(watchedFile)
f.fsync()
d1 := "hello\ngo\n"
f.WriteFile(unwatchedSibling, d1)
f.assertEvents()
}
func TestRemove(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
path := filepath.Join(root, "change")
d1 := "hello\ngo\n"
f.WriteFile(path, d1)
f.watch(path)
f.fsync()
f.events = nil
err := os.Remove(path)
if err != nil {
t.Fatal(err)
}
f.assertEvents(path)
}
func TestRemoveAndAddBack(t *testing.T) {
f := newNotifyFixture(t)
path := filepath.Join(f.paths[0], "change")
d1 := []byte("hello\ngo\n")
err := os.WriteFile(path, d1, 0o644)
if err != nil {
t.Fatal(err)
}
f.watch(path)
f.assertEvents(path)
err = os.Remove(path)
if err != nil {
t.Fatal(err)
}
f.assertEvents(path)
f.events = nil
err = os.WriteFile(path, d1, 0o644)
if err != nil {
t.Fatal(err)
}
f.assertEvents(path)
}
func TestSingleFile(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
path := filepath.Join(root, "change")
d1 := "hello\ngo\n"
f.WriteFile(path, d1)
f.watch(path)
f.fsync()
d2 := []byte("hello\nworld\n")
err := os.WriteFile(path, d2, 0o644)
if err != nil {
t.Fatal(err)
}
f.assertEvents(path)
}
func TestWriteBrokenLink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("no user-space symlinks on windows")
}
f := newNotifyFixture(t)
link := filepath.Join(f.paths[0], "brokenLink")
missingFile := filepath.Join(f.paths[0], "missingFile")
err := os.Symlink(missingFile, link)
if err != nil {
t.Fatal(err)
}
f.assertEvents(link)
}
func TestWriteGoodLink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("no user-space symlinks on windows")
}
f := newNotifyFixture(t)
goodFile := filepath.Join(f.paths[0], "goodFile")
err := os.WriteFile(goodFile, []byte("hello"), 0o644)
if err != nil {
t.Fatal(err)
}
link := filepath.Join(f.paths[0], "goodFileSymlink")
err = os.Symlink(goodFile, link)
if err != nil {
t.Fatal(err)
}
f.assertEvents(goodFile, link)
}
func TestWatchBrokenLink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("no user-space symlinks on windows")
}
f := newNotifyFixture(t)
newRoot, err := NewDir(t.Name())
if err != nil {
t.Fatal(err)
}
defer func() {
err := newRoot.TearDown()
if err != nil {
fmt.Printf("error tearing down temp dir: %v\n", err)
}
}()
link := filepath.Join(newRoot.Path(), "brokenLink")
missingFile := filepath.Join(newRoot.Path(), "missingFile")
err = os.Symlink(missingFile, link)
if err != nil {
t.Fatal(err)
}
f.watch(newRoot.Path())
err = os.Remove(link)
require.NoError(t, err)
f.assertEvents(link)
}
func TestMoveAndReplace(t *testing.T) {
f := newNotifyFixture(t)
root := f.TempDir("root")
file := filepath.Join(root, "myfile")
f.WriteFile(file, "hello")
f.watch(file)
tmpFile := filepath.Join(root, ".myfile.swp")
f.WriteFile(tmpFile, "world")
err := os.Rename(tmpFile, file)
if err != nil {
t.Fatal(err)
}
f.assertEvents(file)
}
func TestWatchBothDirAndFile(t *testing.T) {
f := newNotifyFixture(t)
dir := f.JoinPath("foo")
fileA := f.JoinPath("foo", "a")
fileB := f.JoinPath("foo", "b")
f.WriteFile(fileA, "a")
f.WriteFile(fileB, "b")
f.watch(fileA)
f.watch(dir)
f.fsync()
f.events = nil
f.WriteFile(fileB, "b-new")
f.assertEvents(fileB)
}
func TestWatchNonexistentFileInNonexistentDirectoryCreatedSimultaneously(t *testing.T) {
f := newNotifyFixture(t)
root := f.JoinPath("root")
err := os.Mkdir(root, 0o777)
if err != nil {
t.Fatal(err)
}
file := f.JoinPath("root", "parent", "a")
f.watch(file)
f.fsync()
f.events = nil
f.WriteFile(file, "hello")
f.assertEvents(file)
}
func TestWatchNonexistentDirectory(t *testing.T) {
f := newNotifyFixture(t)
root := f.JoinPath("root")
err := os.Mkdir(root, 0o777)
if err != nil {
t.Fatal(err)
}
parent := f.JoinPath("parent")
file := f.JoinPath("parent", "a")
f.watch(parent)
f.fsync()
f.events = nil
err = os.Mkdir(parent, 0o777)
if err != nil {
t.Fatal(err)
}
// for directories that were the root of an Add, we don't report creation, cf. watcher_darwin.go
f.assertEvents()
f.events = nil
f.WriteFile(file, "hello")
f.assertEvents(file)
}
func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
f := newNotifyFixture(t)
root := f.JoinPath("root")
err := os.Mkdir(root, 0o777)
if err != nil {
t.Fatal(err)
}
parent := f.JoinPath("parent")
file := f.JoinPath("parent", "a")
f.watch(file)
f.assertEvents()
err = os.Mkdir(parent, 0o777)
if err != nil {
t.Fatal(err)
}
f.assertEvents()
f.WriteFile(file, "hello")
f.assertEvents(file)
}
func TestWatchCountInnerFile(t *testing.T) {
f := newNotifyFixture(t)
root := f.paths[0]
a := f.JoinPath(root, "a")
b := f.JoinPath(a, "b")
file := f.JoinPath(b, "bigFile")
f.WriteFile(file, "hello")
f.assertEvents(a, b, file)
expectedWatches := 3
if isRecursiveWatcher() {
expectedWatches = 1
}
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
}
func isRecursiveWatcher() bool {
return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
}
type notifyFixture struct {
ctx context.Context
cancel func()
out *bytes.Buffer
*TempDirFixture
notify Notify
paths []string
events []FileEvent
}
func newNotifyFixture(t *testing.T) *notifyFixture {
out := bytes.NewBuffer(nil)
ctx, cancel := context.WithCancel(t.Context())
nf := ¬ifyFixture{
ctx: ctx,
cancel: cancel,
TempDirFixture: NewTempDirFixture(t),
paths: []string{},
out: out,
}
nf.watch(nf.TempDir("watched"))
t.Cleanup(nf.tearDown)
return nf
}
func (f *notifyFixture) watch(path string) {
f.paths = append(f.paths, path)
f.rebuildWatcher()
}
func (f *notifyFixture) rebuildWatcher() {
// sync any outstanding events and close the old watcher
if f.notify != nil {
f.fsync()
f.closeWatcher()
}
// create a new watcher
notify, err := NewWatcher(f.paths)
if err != nil {
f.T().Fatal(err)
}
f.notify = notify
err = f.notify.Start()
if err != nil {
f.T().Fatal(err)
}
}
func (f *notifyFixture) assertEvents(expected ...string) {
f.fsync()
if runtime.GOOS == "windows" {
// NOTE(nick): It's unclear to me why an extra fsync() helps
// here, but it makes the I/O way more predictable.
f.fsync()
}
if len(f.events) != len(expected) {
f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
}
for i, actual := range f.events {
e := FileEvent(expected[i])
if actual != e {
f.T().Fatalf("Got event %v (expected %v)", actual, e)
}
}
}
func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan error {
done := make(chan error)
go func() {
for {
select {
case <-f.ctx.Done():
close(done)
return
case <-ctx.Done():
close(done)
return
case err := <-f.notify.Errors():
done <- err
close(done)
return
case <-f.notify.Events():
}
}
}()
return done
}
func (f *notifyFixture) fsync() {
f.fsyncWithRetryCount(3)
}
func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
if len(f.paths) == 0 {
return
}
syncPathBase := fmt.Sprintf("sync-%d.txt", time.Now().UnixNano())
syncPath := filepath.Join(f.paths[0], syncPathBase)
anySyncPath := filepath.Join(f.paths[0], "sync-")
timeout := time.After(250 * time.Second)
f.WriteFile(syncPath, time.Now().String())
F:
for {
select {
case <-f.ctx.Done():
return
case err := <-f.notify.Errors():
f.T().Fatal(err)
case event := <-f.notify.Events():
if strings.Contains(string(event), syncPath) {
break F
}
if strings.Contains(string(event), anySyncPath) {
continue
}
// Don't bother tracking duplicate changes to the same path
// for testing.
if len(f.events) > 0 && f.events[len(f.events)-1] == event {
continue
}
f.events = append(f.events, event)
case <-timeout:
if retryCount <= 0 {
f.T().Fatalf("fsync: timeout")
} else {
f.fsyncWithRetryCount(retryCount - 1)
}
return
}
}
}
func (f *notifyFixture) closeWatcher() {
notify := f.notify
err := notify.Close()
if err != nil {
f.T().Fatal(err)
}
// drain channels from watcher
go func() {
for range notify.Events() {
}
}()
go func() {
for range notify.Errors() {
}
}()
}
func (f *notifyFixture) tearDown() {
f.cancel()
f.closeWatcher()
numberOfWatches.Set(0)
}
================================================
FILE: pkg/watch/paths.go
================================================
/*
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 (
"fmt"
"os"
"path/filepath"
)
func greatestExistingAncestor(path string) (string, error) {
if path == string(filepath.Separator) ||
path == fmt.Sprintf("%s%s", filepath.VolumeName(path), string(filepath.Separator)) {
return "", fmt.Errorf("cannot watch root directory")
}
_, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("os.Stat(%q): %w", path, err)
}
if os.IsNotExist(err) {
return greatestExistingAncestor(filepath.Dir(path))
}
return path, nil
}
================================================
FILE: pkg/watch/paths_test.go
================================================
/*
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 (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGreatestExistingAncestor(t *testing.T) {
f := NewTempDirFixture(t)
p, err := greatestExistingAncestor(f.Path())
require.NoError(t, err)
assert.Equal(t, f.Path(), p)
p, err = greatestExistingAncestor(f.JoinPath("missing"))
require.NoError(t, err)
assert.Equal(t, f.Path(), p)
missingTopLevel := "/missingDir/a/b/c"
if runtime.GOOS == "windows" {
missingTopLevel = "C:\\missingDir\\a\\b\\c"
}
_, err = greatestExistingAncestor(missingTopLevel)
assert.Contains(t, err.Error(), "cannot watch root directory")
}
================================================
FILE: pkg/watch/temp.go
================================================
/*
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 (
"os"
"path/filepath"
)
// TempDir holds a temp directory and allows easy access to new temp directories.
type TempDir struct {
dir string
}
// NewDir creates a new TempDir in the default location (typically $TMPDIR)
func NewDir(prefix string) (*TempDir, error) {
return NewDirAtRoot("", prefix)
}
// NewDirAtRoot creates a new TempDir at the given root.
func NewDirAtRoot(root, prefix string) (*TempDir, error) {
tmpDir, err := os.MkdirTemp(root, prefix)
if err != nil {
return nil, err
}
realTmpDir, err := filepath.EvalSymlinks(tmpDir)
if err != nil {
return nil, err
}
return &TempDir{dir: realTmpDir}, nil
}
// NewDirAtSlashTmp creates a new TempDir at /tmp
func NewDirAtSlashTmp(prefix string) (*TempDir, error) {
fullyResolvedPath, err := filepath.EvalSymlinks("/tmp")
if err != nil {
return nil, err
}
return NewDirAtRoot(fullyResolvedPath, prefix)
}
// d.NewDir creates a new TempDir under d
func (d *TempDir) NewDir(prefix string) (*TempDir, error) {
d2, err := os.MkdirTemp(d.dir, prefix)
if err != nil {
return nil, err
}
return &TempDir{d2}, nil
}
func (d *TempDir) NewDeterministicDir(name string) (*TempDir, error) {
d2 := filepath.Join(d.dir, name)
err := os.Mkdir(d2, 0o700)
if os.IsExist(err) {
return nil, err
} else if err != nil {
return nil, err
}
return &TempDir{d2}, nil
}
func (d *TempDir) TearDown() error {
return os.RemoveAll(d.dir)
}
func (d *TempDir) Path() string {
return d.dir
}
// Possible extensions:
// temp file
// named directories or files (e.g., we know we want one git repo for our object, but it should be temporary)
================================================
FILE: pkg/watch/temp_dir_fixture.go
================================================
/*
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 (
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
)
type TempDirFixture struct {
t testing.TB
dir *TempDir
oldDir string
}
// everything not listed in this character class will get replaced by _, so that it's a safe filename
var sanitizeForFilenameRe = regexp.MustCompile("[^a-zA-Z0-9.]")
func SanitizeFileName(name string) string {
return sanitizeForFilenameRe.ReplaceAllString(name, "_")
}
func NewTempDirFixture(t testing.TB) *TempDirFixture {
dir, err := NewDir(SanitizeFileName(t.Name()))
if err != nil {
t.Fatalf("Error making temp dir: %v", err)
}
ret := &TempDirFixture{
t: t,
dir: dir,
}
t.Cleanup(ret.tearDown)
return ret
}
func (f *TempDirFixture) T() testing.TB {
return f.t
}
func (f *TempDirFixture) Path() string {
return f.dir.Path()
}
func (f *TempDirFixture) Chdir() {
cwd, err := os.Getwd()
if err != nil {
f.t.Fatal(err)
}
f.oldDir = cwd
err = os.Chdir(f.Path())
if err != nil {
f.t.Fatal(err)
}
}
func (f *TempDirFixture) JoinPath(path ...string) string {
p := []string{}
isAbs := len(path) > 0 && filepath.IsAbs(path[0])
if isAbs {
if !strings.HasPrefix(path[0], f.Path()) {
f.t.Fatalf("Path outside fixture tempdir are forbidden: %s", path[0])
}
} else {
p = append(p, f.Path())
}
p = append(p, path...)
return filepath.Join(p...)
}
func (f *TempDirFixture) JoinPaths(paths []string) []string {
joined := make([]string, len(paths))
for i, p := range paths {
joined[i] = f.JoinPath(p)
}
return joined
}
// Returns the full path to the file written.
func (f *TempDirFixture) WriteFile(path string, contents string) string {
fullPath := f.JoinPath(path)
base := filepath.Dir(fullPath)
err := os.MkdirAll(base, os.FileMode(0o777))
if err != nil {
f.t.Fatal(err)
}
err = os.WriteFile(fullPath, []byte(contents), os.FileMode(0o777))
if err != nil {
f.t.Fatal(err)
}
return fullPath
}
// Returns the full path to the file written.
func (f *TempDirFixture) CopyFile(originalPath, newPath string) {
contents, err := os.ReadFile(originalPath)
if err != nil {
f.t.Fatal(err)
}
f.WriteFile(newPath, string(contents))
}
// Read the file.
func (f *TempDirFixture) ReadFile(path string) string {
fullPath := f.JoinPath(path)
contents, err := os.ReadFile(fullPath)
if err != nil {
f.t.Fatal(err)
}
return string(contents)
}
func (f *TempDirFixture) WriteSymlink(linkContents, destPath string) {
fullDestPath := f.JoinPath(destPath)
err := os.MkdirAll(filepath.Dir(fullDestPath), os.FileMode(0o777))
if err != nil {
f.t.Fatal(err)
}
err = os.Symlink(linkContents, fullDestPath)
if err != nil {
f.t.Fatal(err)
}
}
func (f *TempDirFixture) MkdirAll(path string) {
fullPath := f.JoinPath(path)
err := os.MkdirAll(fullPath, os.FileMode(0o777))
if err != nil {
f.t.Fatal(err)
}
}
func (f *TempDirFixture) TouchFiles(paths []string) {
for _, p := range paths {
f.WriteFile(p, "")
}
}
func (f *TempDirFixture) Rm(pathInRepo string) {
fullPath := f.JoinPath(pathInRepo)
err := os.RemoveAll(fullPath)
if err != nil {
f.t.Fatal(err)
}
}
func (f *TempDirFixture) NewFile(prefix string) (*os.File, error) {
return os.CreateTemp(f.dir.Path(), prefix)
}
func (f *TempDirFixture) TempDir(prefix string) string {
name, err := os.MkdirTemp(f.dir.Path(), prefix)
if err != nil {
f.t.Fatal(err)
}
return name
}
func (f *TempDirFixture) tearDown() {
if f.oldDir != "" {
err := os.Chdir(f.oldDir)
if err != nil {
f.t.Fatal(err)
}
}
err := f.dir.TearDown()
if err != nil && runtime.GOOS == "windows" &&
(strings.Contains(err.Error(), "The process cannot access the file") ||
strings.Contains(err.Error(), "Access is denied")) {
// NOTE(nick): I'm not convinced that this is a real problem.
// I think it might just be clean up of file notification I/O.
} else if err != nil {
f.t.Fatal(err)
}
}
================================================
FILE: pkg/watch/watcher_darwin.go
================================================
//go:build fsnotify
/*
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 (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsevents"
pathutil "github.com/docker/compose/v5/internal/paths"
)
// A file watcher optimized for Darwin.
// Uses FSEvents to avoid the terrible perf characteristics of kqueue. Requires CGO
type fseventNotify struct {
stream *fsevents.EventStream
events chan FileEvent
errors chan error
stop chan struct{}
pathsWereWatching map[string]any
closeOnce sync.Once
}
func (d *fseventNotify) loop() {
for {
select {
case <-d.stop:
return
case events, ok := <-d.stream.Events:
if !ok {
return
}
for _, e := range events {
e.Path = filepath.Join(string(os.PathSeparator), e.Path)
_, isPathWereWatching := d.pathsWereWatching[e.Path]
if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && e.Flags&fsevents.ItemCreated == fsevents.ItemCreated && isPathWereWatching {
// This is the first create for the path that we're watching. We always get exactly one of these
// even after we get the HistoryDone event. Skip it.
continue
}
d.events <- NewFileEvent(e.Path)
}
}
}
}
// Add a path to be watched. Should only be called during initialization.
func (d *fseventNotify) initAdd(name string) {
d.stream.Paths = append(d.stream.Paths, name)
if d.pathsWereWatching == nil {
d.pathsWereWatching = make(map[string]any)
}
d.pathsWereWatching[name] = struct{}{}
}
func (d *fseventNotify) Start() error {
if len(d.stream.Paths) == 0 {
return nil
}
d.closeOnce = sync.Once{}
numberOfWatches.Add(int64(len(d.stream.Paths)))
err := d.stream.Start()
if err != nil {
return err
}
go d.loop()
return nil
}
func (d *fseventNotify) Close() error {
d.closeOnce.Do(func() {
numberOfWatches.Add(int64(-len(d.stream.Paths)))
d.stream.Stop()
close(d.errors)
close(d.stop)
})
return nil
}
func (d *fseventNotify) Events() chan FileEvent {
return d.events
}
func (d *fseventNotify) Errors() chan error {
return d.errors
}
func newWatcher(paths []string) (Notify, error) {
dw := &fseventNotify{
stream: &fsevents.EventStream{
Latency: 50 * time.Millisecond,
Flags: fsevents.FileEvents | fsevents.IgnoreSelf,
// NOTE(dmiller): this corresponds to the `sinceWhen` parameter in FSEventStreamCreate
// https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
EventID: fsevents.LatestEventID(),
},
events: make(chan FileEvent),
errors: make(chan error),
stop: make(chan struct{}),
}
paths = pathutil.EncompassingPaths(paths)
for _, path := range paths {
path, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("newWatcher: %w", err)
}
dw.initAdd(path)
}
return dw, nil
}
var _ Notify = &fseventNotify{}
================================================
FILE: pkg/watch/watcher_darwin_test.go
================================================
//go:build fsnotify
/*
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"
"gotest.tools/v3/assert"
)
func TestFseventNotifyCloseIdempotent(t *testing.T) {
// Create a watcher with a temporary directory
tmpDir := t.TempDir()
watcher, err := newWatcher([]string{tmpDir})
assert.NilError(t, err)
// Start the watcher
err = watcher.Start()
assert.NilError(t, err)
// Close should work the first time
err = watcher.Close()
assert.NilError(t, err)
// Close should be idempotent - calling it again should not panic
err = watcher.Close()
assert.NilError(t, err)
// Even a third time should be safe
err = watcher.Close()
assert.NilError(t, err)
}
================================================
FILE: pkg/watch/watcher_naive.go
================================================
//go:build !fsnotify
/*
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 (
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/sirupsen/logrus"
"github.com/tilt-dev/fsnotify"
pathutil "github.com/docker/compose/v5/internal/paths"
)
// A naive file watcher that uses the plain fsnotify API.
// Used on all non-Darwin systems (including Windows & Linux).
//
// All OS-specific codepaths are handled by fsnotify.
type naiveNotify struct {
// Paths that we're watching that should be passed up to the caller.
// Note that we may have to watch ancestors of these paths
// in order to fulfill the API promise.
//
// We often need to check if paths are a child of a path in
// the notify list. It might be better to store this in a tree
// structure, so we can filter the list quickly.
notifyList map[string]bool
isWatcherRecursive bool
watcher *fsnotify.Watcher
events chan fsnotify.Event
wrappedEvents chan FileEvent
errors chan error
numWatches int64
}
func (d *naiveNotify) Start() error {
if len(d.notifyList) == 0 {
return nil
}
pathsToWatch := []string{}
for path := range d.notifyList {
pathsToWatch = append(pathsToWatch, path)
}
pathsToWatch, err := greatestExistingAncestors(pathsToWatch)
if err != nil {
return err
}
if d.isWatcherRecursive {
pathsToWatch = pathutil.EncompassingPaths(pathsToWatch)
}
for _, name := range pathsToWatch {
fi, err := os.Stat(name)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("notify.Add(%q): %w", name, err)
}
// if it's a file that doesn't exist,
// we should have caught that above, let's just skip it.
if os.IsNotExist(err) {
continue
}
if fi.IsDir() {
err = d.watchRecursively(name)
if err != nil {
return fmt.Errorf("notify.Add(%q): %w", name, err)
}
} else {
err = d.add(filepath.Dir(name))
if err != nil {
return fmt.Errorf("notify.Add(%q): %w", filepath.Dir(name), err)
}
}
}
go d.loop()
return nil
}
func (d *naiveNotify) watchRecursively(dir string) error {
if d.isWatcherRecursive {
err := d.add(dir)
if err == nil || os.IsNotExist(err) {
return nil
}
return fmt.Errorf("watcher.Add(%q): %w", dir, err)
}
return filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
if d.shouldSkipDir(path) {
logrus.Debugf("Ignoring directory and its contents (recursively): %s", path)
return filepath.SkipDir
}
err = d.add(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("watcher.Add(%q): %w", path, err)
}
return nil
})
}
func (d *naiveNotify) Close() error {
numberOfWatches.Add(-d.numWatches)
d.numWatches = 0
return d.watcher.Close()
}
func (d *naiveNotify) Events() chan FileEvent {
return d.wrappedEvents
}
func (d *naiveNotify) Errors() chan error {
return d.errors
}
func (d *naiveNotify) loop() { //nolint:gocyclo
defer close(d.wrappedEvents)
for e := range d.events {
// The Windows fsnotify event stream sometimes gets events with empty names
// that are also sent to the error stream. Hmmmm...
if e.Name == "" {
continue
}
if e.Op&fsnotify.Create != fsnotify.Create {
if d.shouldNotify(e.Name) {
d.wrappedEvents <- FileEvent(e.Name)
}
continue
}
if d.isWatcherRecursive {
if d.shouldNotify(e.Name) {
d.wrappedEvents <- FileEvent(e.Name)
}
continue
}
// If the watcher is not recursive, we have to walk the tree
// and add watches manually. We fire the event while we're walking the tree.
// because it's a bit more elegant that way.
//
// TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking?
err := filepath.WalkDir(e.Name, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.shouldNotify(path) {
d.wrappedEvents <- FileEvent(path)
}
// TODO(dmiller): symlinks 😭
shouldWatch := false
if info.IsDir() {
// watch directories unless we can skip them entirely
if d.shouldSkipDir(path) {
return filepath.SkipDir
}
shouldWatch = true
} else {
// watch files that are explicitly named, but don't watch others
_, ok := d.notifyList[path]
if ok {
shouldWatch = true
}
}
if shouldWatch {
err := d.add(path)
if err != nil && !os.IsNotExist(err) {
logrus.Infof("Error watching path %s: %s", e.Name, err)
}
}
return nil
})
if err != nil && !os.IsNotExist(err) {
logrus.Infof("Error walking directory %s: %s", e.Name, err)
}
}
}
func (d *naiveNotify) shouldNotify(path string) bool {
if _, ok := d.notifyList[path]; ok {
// We generally don't care when directories change at the root of an ADD
stat, err := os.Lstat(path)
isDir := err == nil && stat.IsDir()
return !isDir
}
for root := range d.notifyList {
if pathutil.IsChild(root, path) {
return true
}
}
return false
}
func (d *naiveNotify) shouldSkipDir(path string) bool {
// If path is directly in the notifyList, we should always watch it.
if d.notifyList[path] {
return false
}
// Suppose we're watching
// /src/.tiltignore
// but the .tiltignore file doesn't exist.
//
// Our watcher will create an inotify watch on /src/.
//
// But then we want to make sure we don't recurse from /src/ down to /src/node_modules.
//
// To handle this case, we only want to traverse dirs that are:
// - A child of a directory that's in our notify list, or
// - A parent of a directory that's in our notify list
// (i.e., to cover the "path doesn't exist" case).
for root := range d.notifyList {
if pathutil.IsChild(root, path) || pathutil.IsChild(path, root) {
return false
}
}
return true
}
func (d *naiveNotify) add(path string) error {
err := d.watcher.Add(path)
if err != nil {
return err
}
d.numWatches++
numberOfWatches.Add(1)
return nil
}
func newWatcher(paths []string) (Notify, error) {
fsw, err := fsnotify.NewWatcher()
if err != nil {
if strings.Contains(err.Error(), "too many open files") && runtime.GOOS == "linux" {
return nil, fmt.Errorf("hit OS limits creating a watcher.\n" +
"Run 'sysctl fs.inotify.max_user_instances' to check your inotify limits.\n" +
"To raise them, run 'sudo sysctl fs.inotify.max_user_instances=1024'")
}
return nil, fmt.Errorf("creating file watcher: %w", err)
}
MaybeIncreaseBufferSize(fsw)
err = fsw.SetRecursive()
isWatcherRecursive := err == nil
wrappedEvents := make(chan FileEvent)
notifyList := make(map[string]bool, len(paths))
if isWatcherRecursive {
paths = pathutil.EncompassingPaths(paths)
}
for _, path := range paths {
path, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("newWatcher: %w", err)
}
notifyList[path] = true
}
wmw := &naiveNotify{
notifyList: notifyList,
watcher: fsw,
events: fsw.Events,
wrappedEvents: wrappedEvents,
errors: fsw.Errors,
isWatcherRecursive: isWatcherRecursive,
}
return wmw, nil
}
var _ Notify = &naiveNotify{}
func greatestExistingAncestors(paths []string) ([]string, error) {
result := []string{}
for _, p := range paths {
newP, err := greatestExistingAncestor(p)
if err != nil {
return nil, fmt.Errorf("finding ancestor of %s: %w", p, err)
}
result = append(result, newP)
}
return result, nil
}
================================================
FILE: pkg/watch/watcher_naive_test.go
================================================
/*
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 (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestDontWatchEachFile(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("This test uses linux-specific inotify checks")
}
// fsnotify is not recursive, so we need to watch each directory
// you can watch individual files with fsnotify, but that is more prone to exhaust resources
// this test uses a Linux way to get the number of watches to make sure we're watching
// per-directory, not per-file
f := newNotifyFixture(t)
watched := f.TempDir("watched")
// there are a few different cases we want to test for because the code paths are slightly
// different:
// 1) initial: data there before we ever call watch
// 2) inplace: data we create while the watch is happening
// 3) staged: data we create in another directory and then atomically move into place
// initial
f.WriteFile(f.JoinPath(watched, "initial.txt"), "initial data")
initialDir := f.JoinPath(watched, "initial_dir")
if err := os.Mkdir(initialDir, 0o777); err != nil {
t.Fatal(err)
}
for i := range 100 {
f.WriteFile(f.JoinPath(initialDir, fmt.Sprintf("%d", i)), "initial data")
}
f.watch(watched)
f.fsync()
if len(f.events) != 0 {
t.Fatalf("expected 0 initial events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
// inplace
inplace := f.JoinPath(watched, "inplace")
if err := os.Mkdir(inplace, 0o777); err != nil {
t.Fatal(err)
}
f.WriteFile(f.JoinPath(inplace, "inplace.txt"), "inplace data")
inplaceDir := f.JoinPath(inplace, "inplace_dir")
if err := os.Mkdir(inplaceDir, 0o777); err != nil {
t.Fatal(err)
}
for i := range 100 {
f.WriteFile(f.JoinPath(inplaceDir, fmt.Sprintf("%d", i)), "inplace data")
}
f.fsync()
if len(f.events) < 100 {
t.Fatalf("expected >100 inplace events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
// staged
staged := f.TempDir("staged")
f.WriteFile(f.JoinPath(staged, "staged.txt"), "staged data")
stagedDir := f.JoinPath(staged, "staged_dir")
if err := os.Mkdir(stagedDir, 0o777); err != nil {
t.Fatal(err)
}
for i := range 100 {
f.WriteFile(f.JoinPath(stagedDir, fmt.Sprintf("%d", i)), "staged data")
}
if err := os.Rename(staged, f.JoinPath(watched, "staged")); err != nil {
t.Fatal(err)
}
f.fsync()
if len(f.events) < 100 {
t.Fatalf("expected >100 staged events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
n, err := inotifyNodes()
require.NoError(t, err)
if n > 10 {
t.Fatalf("watching more than 10 files: %d", n)
}
}
func inotifyNodes() (int, error) {
pid := os.Getpid()
output, err := exec.Command("/bin/sh", "-c", fmt.Sprintf(
"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
if err != nil {
return 0, fmt.Errorf("error running command to determine number of watched files: %w\n %s", err, output)
}
n, err := strconv.Atoi(strings.TrimSpace(string(output)))
if err != nil {
return 0, fmt.Errorf("couldn't parse number of watched files: %w", err)
}
return n, nil
}
func TestDontRecurseWhenWatchingParentsOfNonExistentFiles(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("This test uses linux-specific inotify checks")
}
f := newNotifyFixture(t)
watched := f.TempDir("watched")
f.watch(filepath.Join(watched, ".tiltignore"))
excludedDir := f.JoinPath(watched, "excluded")
for i := range 10 {
f.WriteFile(f.JoinPath(excludedDir, fmt.Sprintf("%d", i), "data.txt"), "initial data")
}
f.fsync()
n, err := inotifyNodes()
require.NoError(t, err)
if n > 5 {
t.Fatalf("watching more than 5 files: %d", n)
}
}
================================================
FILE: pkg/watch/watcher_nonwin.go
================================================
//go:build !windows
/*
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 "github.com/tilt-dev/fsnotify"
func MaybeIncreaseBufferSize(w *fsnotify.Watcher) {
// Not needed on non-windows
}
================================================
FILE: pkg/watch/watcher_windows.go
================================================
//go:build windows
/*
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 (
"github.com/tilt-dev/fsnotify"
)
// TODO(nick): I think the ideal API would be to automatically increase the
// size of the buffer when we exceed capacity. But this gets messy,
// because each time we get a short read error, we need to invalidate
// everything we know about the currently changed files. So for now,
// we just provide a way for people to increase the buffer ourselves.
//
// It might also pay to be clever about sizing the buffer
// relative the number of files in the directory we're watching.
func MaybeIncreaseBufferSize(w *fsnotify.Watcher) {
w.SetBufferSize(DesiredWindowsBufferSize())
}