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** <!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" --> **(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 <table class="tg"> <col width="45%"> <col width="65%"> <tr> <td>Community Slack</td> <td> The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/community/" target="_blank">with this link</a>. </td> </tr> <tr> <td>Forums</td> <td> 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 <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>. </td> </tr> <tr> <td>Twitter</td> <td> You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a> to get updates on our products. You can also tweet us questions or just share blogs or stories. </td> </tr> <tr> <td>Stack Overflow</td> <td> Stack Overflow has over 17000 Docker questions listed. We regularly monitor <a href="https://stackoverflow.com/questions/tagged/docker" target="_blank">Docker questions</a> and so do many other knowledgeable Docker users. </td> </tr> </table> ### 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 <joe.smith@email.com> 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 <<EOT set -e git add -A cp -rf /out/* . diff=$(git status --porcelain -- go.mod go.sum) if [ -n "$diff" ]; then echo >&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 <<EOT set -e rsync -a /context/. . docsgen --formats "$DOCS_FORMATS" --source "docs/reference" mkdir /out cp -r docs/reference /out EOT FROM scratch AS docs-update COPY --from=docs-build /out /out FROM docs-build AS docs-validate RUN --mount=target=/context \ --mount=target=.,type=tmpfs <<EOT set -e rsync -a /context/. . git add -A rm -rf docs/reference/* cp -rf /out/* ./docs/ if [ -n "$(git status --porcelain -- docs/reference)" ]; then echo >&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 [![GitHub release](https://img.shields.io/github/v/release/docker/compose.svg?style=flat-square)](https://github.com/docker/compose/releases/latest) [![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v5) [![Build Status](https://img.shields.io/github/actions/workflow/status/docker/compose/ci.yml?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci) [![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v5?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v5) [![Codecov](https://codecov.io/gh/docker/compose/branch/main/graph/badge.svg?token=HP3K4Y4ctu)](https://codecov.io/gh/docker/compose) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/docker/compose/badge)](https://api.securityscorecards.dev/projects/github.com/docker/compose) ![Docker Compose](logo.png?raw=true "Docker Compose Logo") 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 := "<none>", "<none>" 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 <hannibal@a-team.com>")`) 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 = "<none>" } tag := img.Tag if tag == "" { tag = "<none>" } 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 = "<unset>" 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 <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 `<provider> compose --project-name <NAME> down <SERVICE>` 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 <arg>...] [options] [COMMAND] [ARGS...] ``` <!---MARKER_GEN_START--> 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<br>(default: the path of the, first specified, Compose file) | | `-p`, `--project-name` | `string` | | Project name | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_dry-run.md ================================================ # docker compose alpha dry-run <!---MARKER_GEN_START--> Dry run command allows you to test a command without applying changes <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_generate.md ================================================ # docker compose alpha generate <!---MARKER_GEN_START--> EXPERIMENTAL - Generate a Compose file from existing containers ### Options | Name | Type | Default | Description | |:----------------|:---------|:--------|:------------------------------------------| | `--dry-run` | `bool` | | Execute command in dry run mode | | `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] | | `--name` | `string` | | Project name to set in the Compose file | | `--project-dir` | `string` | | Directory to use for the project | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_publish.md ================================================ # docker compose alpha publish <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_scale.md ================================================ # docker compose alpha scale <!---MARKER_GEN_START--> Scale services ### Options | Name | Type | Default | Description | |:------------|:-----|:--------|:--------------------------------| | `--dry-run` | | | Execute command in dry run mode | | `--no-deps` | | | Don't start linked services | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_viz.md ================================================ # docker compose alpha viz <!---MARKER_GEN_START--> 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,<br>otherwise tab character '\t' will be used | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_alpha_watch.md ================================================ # docker compose alpha watch <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_attach.md ================================================ # docker compose attach <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_bridge.md ================================================ # docker compose bridge <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_bridge_convert.md ================================================ # docker compose bridge convert <!---MARKER_GEN_START--> 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) | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_bridge_transformations.md ================================================ # docker compose bridge transformations <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_bridge_transformations_create.md ================================================ # docker compose bridge transformations create <!---MARKER_GEN_START--> 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) | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_bridge_transformations_list.md ================================================ # docker compose bridge transformations list <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_build.md ================================================ # docker compose build <!---MARKER_GEN_START--> 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) | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> Create a new image from a service container's changes ### Options | Name | Type | Default | Description | |:------------------|:---------|:--------|:-----------------------------------------------------------| | `-a`, `--author` | `string` | | Author (e.g., "John Hannibal Smith <hannibal@a-team.com>") | | `-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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_config.md ================================================ # docker compose convert <!---MARKER_GEN_START--> `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. | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_create.md ================================================ # docker compose create <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_down.md ================================================ # docker compose down <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> Export a service container's filesystem as a tar archive ### Options | Name | Type | Default | Description | |:-----------------|:---------|:--------|:---------------------------------------------------------| | `--dry-run` | `bool` | | Execute command in dry run mode | | `--index` | `int` | `0` | index of the container if service has multiple replicas. | | `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_images.md ================================================ # docker compose images <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_kill.md ================================================ # docker compose kill <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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) | <!---MARKER_GEN_END--> ## Description Displays log output from services ================================================ FILE: docs/reference/compose_ls.md ================================================ # docker compose ls <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## Description Lists running Compose projects ================================================ FILE: docs/reference/compose_pause.md ================================================ # docker compose pause <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## Description Pauses running containers of a service. They can be unpaused with `docker compose unpause`. ================================================ FILE: docs/reference/compose_port.md ================================================ # docker compose port <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## Description Prints the public port for a port binding ================================================ FILE: docs/reference/compose_ps.md ================================================ # docker compose ps <!---MARKER_GEN_START--> 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:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>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] | <!---MARKER_GEN_END--> ## 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 ### <a name="format"></a> 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" } ] } ] ``` ### <a name="status"></a> 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) ``` ### <a name="filter"></a> Filter containers by status (--filter) The [`--status` flag](#status) is a convenient shorthand for the `--filter status=<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=<status>` option, but additional filter options may be added in the future. ================================================ FILE: docs/reference/compose_publish.md ================================================ # docker compose publish <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_pull.md ================================================ # docker compose pull <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> Scale services ### Options | Name | Type | Default | Description | |:------------|:-------|:--------|:--------------------------------| | `--dry-run` | `bool` | | Execute command in dry run mode | | `--no-deps` | `bool` | | Don't start linked services | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_start.md ================================================ # docker compose start <!---MARKER_GEN_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 | <!---MARKER_GEN_END--> ## Description Starts existing containers for a service ================================================ FILE: docs/reference/compose_stats.md ================================================ # docker compose stats <!---MARKER_GEN_START--> 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:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_stop.md ================================================ # docker compose stop <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> Displays the running processes ### Options | Name | Type | Default | Description | |:------------|:-------|:--------|:--------------------------------| | `--dry-run` | `bool` | | Execute command in dry run mode | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> Unpauses paused containers of a service ### Options | Name | Type | Default | Description | |:------------|:-------|:--------|:--------------------------------| | `--dry-run` | `bool` | | Execute command in dry run mode | <!---MARKER_GEN_END--> ## Description Unpauses paused containers of a service ================================================ FILE: docs/reference/compose_up.md ================================================ # docker compose up <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ## 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 <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_volumes.md ================================================ # docker compose volumes <!---MARKER_GEN_START--> 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:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display volume names | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_wait.md ================================================ # docker compose wait <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ FILE: docs/reference/compose_watch.md ================================================ # docker compose watch <!---MARKER_GEN_START--> 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 | <!---MARKER_GEN_END--> ================================================ 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 <hannibal@a-team.com>") 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=<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=<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 <adrg@epistack.com> 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 <adrg@epistack.com> 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 <adrg@epistack.com> 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:<name|id>. // > 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 <portnum>/[<proto>] // or <startport-endport>/[<proto>] 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=<n>] MODEL [-- <runtime-flags...>] 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<<font point-size="15">service name</font> graphBuilder.WriteString(opts.Indentation) writeQuoted(graphBuilder, serviceNode.Name) graphBuilder.WriteString(" [style=\"filled\" label=<<font point-size=\"15\">") graphBuilder.WriteString(serviceNode.Name) graphBuilder.WriteString("</font>") if opts.IncludeNetworks && len(serviceNode.Networks) > 0 { graphBuilder.WriteString("<font point-size=\"10\">") graphBuilder.WriteString("<br/><br/><b>Networks:</b>") for _, networkName := range serviceNode.NetworksByPriority() { graphBuilder.WriteString("<br/>") graphBuilder.WriteString(networkName) } graphBuilder.WriteString("</font>") } if opts.IncludePorts && len(serviceNode.Ports) > 0 { graphBuilder.WriteString("<font point-size=\"10\">") graphBuilder.WriteString("<br/><br/><b>Ports:</b>") for _, portConfig := range serviceNode.Ports { graphBuilder.WriteString("<br/>") 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("</font>") } if opts.IncludeImageName { graphBuilder.WriteString("<font point-size=\"10\">") graphBuilder.WriteString("<br/><br/><b>Image:</b><br/>") graphBuilder.WriteString(api.GetImageNameOrDefault(*serviceNode, projectName)) graphBuilder.WriteString("</font>") } 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 <hannibal@a-team.com>", "-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 <hannibal@a-team.com>", "-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 <hannibal@a-team.com>", "-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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) <-- 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 <KEY[=VAL]>) // 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 <none>", 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 ================================================ <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>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()) }