diff --git a/pkg/api/context.go b/pkg/api/context.go new file mode 100644 index 000000000..af49c5d24 --- /dev/null +++ b/pkg/api/context.go @@ -0,0 +1,32 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES 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) +} diff --git a/pkg/api/io.go b/pkg/api/io.go index 6aa77eaf2..44bf30088 100644 --- a/pkg/api/io.go +++ b/pkg/api/io.go @@ -17,12 +17,27 @@ package api import ( - "github.com/docker/cli/cli/streams" + "io" ) -// Streams defines the standard streams (stdin, stdout, stderr) used by the CLI. -type Streams interface { - Out() *streams.Out - Err() *streams.Out - In() *streams.In +// OutputStream is a writable stream with terminal detection capabilities +type OutputStream interface { + io.Writer + + // IsTerminal returns true if the stream is connected to a terminal + IsTerminal() bool + + // FD returns the file descriptor for the stream + FD() uintptr +} + +// InputStream is a readable stream with terminal detection capabilities +type InputStream interface { + io.Reader + + // IsTerminal returns true if the stream is connected to a terminal + IsTerminal() bool + + // FD returns the file descriptor for the stream + FD() uintptr } diff --git a/pkg/compose/apiSocket.go b/pkg/compose/apiSocket.go index 1c347528e..ddc7a0290 100644 --- a/pkg/compose/apiSocket.go +++ b/pkg/compose/apiSocket.go @@ -41,7 +41,7 @@ func (s *composeService) useAPISocket(project *types.Project) (*types.Project, e return project, nil } - if s.dockerCli.ServerInfo().OSType == "windows" { + if s.getContextInfo().ServerOSType() == "windows" { return nil, errors.New("use_api_socket can't be used with a Windows Docker Engine") } @@ -49,6 +49,7 @@ func (s *composeService) useAPISocket(project *types.Project) (*types.Project, e if err != nil { return nil, fmt.Errorf("resolving credentials failed: %w", err) } + newConfig := &configfile.ConfigFile{ AuthConfigs: creds, } diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 4fa3fa309..8e2b3e471 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -29,10 +29,8 @@ import ( "github.com/containerd/platforms" "github.com/docker/buildx/build" "github.com/docker/buildx/builder" - "github.com/docker/buildx/store/storeutil" "github.com/docker/buildx/util/buildflags" xprogress "github.com/docker/buildx/util/progress" - "github.com/docker/cli/cli/command" cliopts "github.com/docker/cli/opts" "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" @@ -143,7 +141,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti } // Initialize buildkit nodes - buildkitEnabled, err := s.dockerCli.BuildKitEnabled() + buildkitEnabled, err := s.getContextInfo().BuildKitEnabled() if err != nil { return nil, err } @@ -384,7 +382,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ // // Finally, standard proxy variables based on the Docker client configuration are added, but will not overwrite // any values if already present. -func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, opts api.BuildOptions) types.MappingWithEquals { +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). @@ -392,7 +390,7 @@ func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, ser // proxy arguments do NOT override and should NOT have env resolution applied, // so they're handled last - for k, v := range storeutil.GetProxyConfig(dockerCli) { + for k, v := range proxyConfig { if _, ok := result[k]; !ok { v := v result[k] = &v @@ -502,7 +500,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se CacheTo: build.CreateCaches(cacheTo), NoCache: service.Build.NoCache, Pull: service.Build.Pull, - BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)), + BuildArgs: flatten(resolveAndMergeBuildArgs(s.getProxyConfig(), project, service, options)), Tags: tags, Target: service.Build.Target, Exports: exports, diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index fa682e62e..a793151f3 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -139,10 +139,10 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project displayMode := progressui.DisplayMode(options.Progress) out := options.Out if out == nil { - if displayMode == progress.ModeAuto && !s.dockerCli.Out().IsTerminal() { + if displayMode == progress.ModeAuto && !s.stdout().IsTerminal() { displayMode = progressui.PlainMode } - out = os.Stdout // should be s.dockerCli.Out(), but NewDisplay require access to the underlying *File + out = os.Stdout // should be s.stdout(), but NewDisplay require access to the underlying *File } display, err := progressui.NewDisplay(out, displayMode) if err != nil { @@ -185,7 +185,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project build := *service.Build labels := getImageBuildLabels(project, service) - args := resolveAndMergeBuildArgs(s.dockerCli, project, service, options).ToMapping() + args := resolveAndMergeBuildArgs(s.getProxyConfig(), project, service, options).ToMapping() for k, v := range args { args[k] = strings.ReplaceAll(v, "${", "$${") } diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go index a84929193..fb3fd2967 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -28,7 +28,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image/build" "github.com/docker/compose/v2/pkg/api" buildtypes "github.com/docker/docker/api/types/build" @@ -175,7 +174,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj RegistryToken: authConfig.RegistryToken, } } - buildOpts := imageBuildOptions(s.dockerCli, project, service, options) + buildOpts := imageBuildOptions(s.getProxyConfig(), project, service, options) imageName := api.GetImageNameOrDefault(service, project.Name) buildOpts.Tags = append(buildOpts.Tags, imageName) buildOpts.Dockerfile = relDockerfile @@ -215,7 +214,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj return imageID, nil } -func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) buildtypes.ImageBuildOptions { +func imageBuildOptions(proxyConfigs map[string]string, project *types.Project, service types.ServiceConfig, options api.BuildOptions) buildtypes.ImageBuildOptions { config := service.Build return buildtypes.ImageBuildOptions{ Version: buildtypes.BuilderV1, @@ -223,7 +222,7 @@ func imageBuildOptions(dockerCli command.Cli, project *types.Project, service ty NoCache: config.NoCache, Remove: true, PullParent: config.Pull, - BuildArgs: resolveAndMergeBuildArgs(dockerCli, project, service, options), + BuildArgs: resolveAndMergeBuildArgs(proxyConfigs, project, service, options), Labels: config.Labels, NetworkMode: config.Network, ExtraHosts: config.ExtraHosts.AsList(":"), diff --git a/pkg/compose/commit.go b/pkg/compose/commit.go index d46667910..0edac4335 100644 --- a/pkg/compose/commit.go +++ b/pkg/compose/commit.go @@ -40,8 +40,6 @@ func (s *composeService) commit(ctx context.Context, projectName string, options return err } - clnt := s.apiClient() - w := progress.ContextWriter(ctx) name := getCanonicalContainerName(ctr) @@ -65,7 +63,7 @@ func (s *composeService) commit(ctx context.Context, projectName string, options return nil } - response, err := clnt.ContainerCommit(ctx, ctr.ID, container.CommitOptions{ + response, err := s.apiClient().ContainerCommit(ctx, ctr.ID, container.CommitOptions{ Reference: options.Reference, Comment: options.Comment, Author: options.Author, diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 45de82b50..fd096f06c 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -20,12 +20,14 @@ import ( "context" "errors" "fmt" + "io" "os" "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" @@ -53,7 +55,26 @@ func init() { type Option func(service *composeService) error -// NewComposeService create a local implementation of the compose.Compose API +// 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, @@ -76,6 +97,56 @@ func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, e return s, nil } +// WithStreams sets custom I/O streams for output and interaction +func WithStreams(out, err api.OutputStream, in api.InputStream) 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 api.OutputStream) Option { + return func(s *composeService) error { + s.outStream = out + return nil + } +} + +// WithErrorStream sets a custom error stream +func WithErrorStream(err api.OutputStream) Option { + return func(s *composeService) error { + s.errStream = err + return nil + } +} + +// WithInputStream sets a custom input stream +func WithInputStream(in api.InputStream) 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 { @@ -119,6 +190,13 @@ type composeService struct { // prompt is used to interact with user and confirm actions prompt Prompt + // Optional overrides for specific components (for SDK users) + outStream api.OutputStream + errStream api.OutputStream + inStream api.InputStream + contextInfo api.ContextInfo + proxyConfig map[string]string + clock clockwork.Clock maxConcurrency int dryRun bool @@ -144,23 +222,65 @@ 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 { + // If stream overrides are provided, use them + if s.outStream != nil { + return streams.NewOut(s.outStream) + } return s.dockerCli.Out() } func (s *composeService) stdin() *streams.In { + // If stream overrides are provided, use them + if s.inStream != nil { + return streams.NewIn(&readCloserAdapter{r: s.inStream}) + } return s.dockerCli.In() } func (s *composeService) stderr() *streams.Out { + // If stream overrides are provided, use them + if s.errStream != nil { + return streams.NewOut(s.errStream) + } return s.dockerCli.Err() } func (s *composeService) stdinfo() *streams.Out { if stdioToStdout { - return s.dockerCli.Out() + return s.stdout() } - return s.dockerCli.Err() + return s.stderr() +} + +// 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 } func getCanonicalContainerName(c container.Summary) string { diff --git a/pkg/compose/docker_cli_providers.go b/pkg/compose/docker_cli_providers.go new file mode 100644 index 000000000..207fa3e37 --- /dev/null +++ b/pkg/compose/docker_cli_providers.go @@ -0,0 +1,38 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS 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() +} diff --git a/pkg/compose/export.go b/pkg/compose/export.go index 73161cf5f..21dc8ad20 100644 --- a/pkg/compose/export.go +++ b/pkg/compose/export.go @@ -43,15 +43,13 @@ func (s *composeService) export(ctx context.Context, projectName string, options } if options.Output == "" { - if s.dockerCli.Out().IsTerminal() { + 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) } - clnt := s.apiClient() - w := progress.ContextWriter(ctx) name := getCanonicalContainerName(container) @@ -64,7 +62,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options StatusText: "Exporting", }) - responseBody, err := clnt.ContainerExport(ctx, container.ID) + responseBody, err := s.apiClient().ContainerExport(ctx, container.ID) if err != nil { return err } @@ -82,7 +80,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options if !s.dryRun { if options.Output == "" { - _, err := io.Copy(s.dockerCli.Out(), responseBody) + _, err := io.Copy(s.stdout(), responseBody) return err } else { writer, err := atomicwriter.New(options.Output, 0o600) diff --git a/pkg/compose/shellout.go b/pkg/compose/shellout.go index 29ba255b8..b7b90c1d8 100644 --- a/pkg/compose/shellout.go +++ b/pkg/compose/shellout.go @@ -52,8 +52,10 @@ func (s *composeService) prepareShellOut(gctx context.Context, env types.Mapping 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 { diff --git a/pkg/compose/wait.go b/pkg/compose/wait.go index 6cf88b0b3..a2ee22948 100644 --- a/pkg/compose/wait.go +++ b/pkg/compose/wait.go @@ -42,7 +42,7 @@ func (s *composeService) Wait(ctx context.Context, projectName string, options a select { case result := <-resultC: - _, _ = fmt.Fprintf(s.dockerCli.Out(), "container %q exited with status code %d\n", ctr.ID, result.StatusCode) + _, _ = fmt.Fprintf(s.stdout(), "container %q exited with status code %d\n", ctr.ID, result.StatusCode) statusCode = result.StatusCode case err = <-errC: }