mirror of
https://github.com/docker/compose.git
synced 2026-10-09 13:40:37 +00:00
Introduce abstractions to support SDK usage without requiring Docker CLI
This commit prepares the Compose service for SDK usage by abstracting away the hard dependency on command.Cli. The Docker CLI remains the standard path for the CLI tool, but SDK users can now provide custom implementations of streams and context information. Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
parent
5924387e89
commit
e1678c5c43
12 changed files with 233 additions and 32 deletions
32
pkg/api/context.go
Normal file
32
pkg/api/context.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, "${", "$${")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(":"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
38
pkg/compose/docker_cli_providers.go
Normal file
38
pkg/compose/docker_cli_providers.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue