From c53e5ee36269c7104b086aafe98e279c62f66b83 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:50:29 +0100 Subject: [PATCH 1/2] Add LoadProject method to Compose SDK API This commit adds a new LoadProject method to the Compose service API, allowing SDK users to programmatically load Compose projects with full control over the loading process. Changes: 1. New API method (pkg/api/api.go): - LoadProject(ctx, ProjectLoadOptions) (*types.Project, error) - ProjectLoadOptions struct with all loader configuration - LoadListener callback for event notifications (metrics, etc.) - ProjectOptionsFns field for compose-go loader options 2. Implementation (pkg/compose/loader.go): - createRemoteLoaders: Git and OCI remote loader setup - buildProjectOptions: Translates ProjectLoadOptions to compose-go options - postProcessProject: Service filtering, labels, resource pruning 3. Unit test (pkg/compose/loader_test.go): - Tests basic project loading functionality - Verifies ProjectOptionsFns with cli.WithoutEnvironmentResolution 4. Mock update (pkg/mocks/mock_docker_compose_api.go): - Added LoadProject to mock interface Key design decisions: - LoadListener pattern keeps metrics collection in CLI, not SDK - ProjectOptionsFns exposes compose-go options directly (e.g., cli.WithInterpolation(false)) - Post-processing in SDK: labels, service filtering, resource pruning - Environment resolution NOT in SDK (command responsibility) - Compatibility mode handling (api.Separator) Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/api/api.go | 43 ++++ pkg/compose/loader.go | 149 ++++++++++++ pkg/compose/loader_test.go | 343 +++++++++++++++++++++++++++ pkg/mocks/mock_docker_compose_api.go | 277 ++++++++++----------- 4 files changed, 676 insertions(+), 136 deletions(-) create mode 100644 pkg/compose/loader.go create mode 100644 pkg/compose/loader_test.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 427520beb2b..d78260dd547 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -24,12 +24,53 @@ import ( "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/docker/docker/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 +} + // 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 { @@ -102,6 +143,8 @@ type Compose interface { // GetConfiguredStreams returns the configured I/O streams (stdout, stderr, stdin). // If no custom streams were configured, it returns the dockerCli streams. GetConfiguredStreams() (stdout io.Writer, stderr io.Writer, stdin io.Reader) + // LoadProject loads and validates a Compose project from configuration files. + LoadProject(ctx context.Context, options ProjectLoadOptions) (*types.Project, error) } type VolumesOptions struct { diff --git a/pkg/compose/loader.go b/pkg/compose/loader.go new file mode 100644 index 00000000000..311181161b6 --- /dev/null +++ b/pkg/compose/loader.go @@ -0,0 +1,149 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR 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/v2/pkg/api" + "github.com/docker/compose/v2/pkg/remote" +) + +// 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.Offline) + + 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 { + 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(offline bool) []loader.ResourceLoader { + if offline { + return nil + } + git := remote.NewGitRemoteLoader(s.dockerCli, offline) + oci := remote.NewOCIRemoteLoader(s.dockerCli, offline) + 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, + cli.WithEnvFiles(options.EnvFiles...), + cli.WithDotEnv, + cli.WithConfigFileEnv, + cli.WithDefaultConfigPath, + cli.WithEnvFiles(options.EnvFiles...), + cli.WithDotEnv, + 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 +} diff --git a/pkg/compose/loader_test.go b/pkg/compose/loader_test.go new file mode 100644 index 00000000000..d5c345ad278 --- /dev/null +++ b/pkg/compose/loader_test.go @@ -0,0 +1,343 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR 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" + "path/filepath" + "testing" + + "github.com/compose-spec/compose-go/v2/cli" + "github.com/docker/compose/v2/pkg/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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) + + // Create compose service + service, err := NewComposeService(nil) + require.NoError(t, err) + + // Load the project + ctx := context.Background() + project, err := service.LoadProject(ctx, 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 + require.NoError(t, os.Setenv("TEST_VAR", "resolved_value")) + t.Cleanup(func() { + require.NoError(t, os.Unsetenv("TEST_VAR")) + }) + + service, err := NewComposeService(nil) + require.NoError(t, err) + + ctx := context.Background() + + // Test with environment resolution (default) + t.Run("WithResolution", func(t *testing.T) { + project, err := service.LoadProject(ctx, 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(ctx, 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) + + ctx := context.Background() + + // Load only specific services + project, err := service.LoadProject(ctx, 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) + + ctx := context.Background() + + // Without debug profile + t.Run("WithoutProfile", func(t *testing.T) { + project, err := service.LoadProject(ctx, 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(ctx, 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) + + ctx := context.Background() + + // Track events received + var events []string + listener := func(event string, metadata map[string]any) { + events = append(events, event) + } + + project, err := service.LoadProject(ctx, 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) + + ctx := context.Background() + + // Without explicit project name + t.Run("InferredName", func(t *testing.T) { + project, err := service.LoadProject(ctx, 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(ctx, 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) + + ctx := context.Background() + + // With compatibility mode + project, err := service.LoadProject(ctx, 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) + + ctx := context.Background() + + // Should return an error for invalid YAML + project, err := service.LoadProject(ctx, 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) + + ctx := context.Background() + + // Should return an error for missing file + project, err := service.LoadProject(ctx, api.ProjectLoadOptions{ + ConfigPaths: []string{"/nonexistent/compose.yaml"}, + }) + + require.Error(t, err) + assert.Nil(t, project) +} diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index be7cea3ca20..be1020176fb 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -11,6 +11,7 @@ package mocks import ( context "context" + io "io" reflect "reflect" types "github.com/compose-spec/compose-go/v2/types" @@ -18,31 +19,31 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockService is a mock of Service interface. -type MockService struct { +// MockCompose is a mock of Compose interface. +type MockCompose struct { ctrl *gomock.Controller - recorder *MockServiceMockRecorder + recorder *MockComposeMockRecorder } -// MockServiceMockRecorder is the mock recorder for MockService. -type MockServiceMockRecorder struct { - mock *MockService +// MockComposeMockRecorder is the mock recorder for MockCompose. +type MockComposeMockRecorder struct { + mock *MockCompose } -// NewMockService creates a new mock instance. -func NewMockService(ctrl *gomock.Controller) *MockService { - mock := &MockService{ctrl: ctrl} - mock.recorder = &MockServiceMockRecorder{mock} +// 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 *MockService) EXPECT() *MockServiceMockRecorder { +func (m *MockCompose) EXPECT() *MockComposeMockRecorder { return m.recorder } // Attach mocks base method. -func (m *MockService) Attach(ctx context.Context, projectName string, options api.AttachOptions) error { +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) @@ -50,13 +51,13 @@ func (m *MockService) Attach(ctx context.Context, projectName string, options ap } // Attach indicates an expected call of Attach. -func (mr *MockServiceMockRecorder) Attach(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Attach), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attach", reflect.TypeOf((*MockCompose)(nil).Attach), ctx, projectName, options) } // Build mocks base method. -func (m *MockService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error { +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) @@ -64,13 +65,13 @@ func (m *MockService) Build(ctx context.Context, project *types.Project, options } // Build indicates an expected call of Build. -func (mr *MockServiceMockRecorder) Build(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Build), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockCompose)(nil).Build), ctx, project, options) } // Commit mocks base method. -func (m *MockService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error { +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) @@ -78,13 +79,13 @@ func (m *MockService) Commit(ctx context.Context, projectName string, options ap } // Commit indicates an expected call of Commit. -func (mr *MockServiceMockRecorder) Commit(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Commit), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockCompose)(nil).Commit), ctx, projectName, options) } // Copy mocks base method. -func (m *MockService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error { +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) @@ -92,13 +93,13 @@ func (m *MockService) Copy(ctx context.Context, projectName string, options api. } // Copy indicates an expected call of Copy. -func (mr *MockServiceMockRecorder) Copy(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Copy), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockCompose)(nil).Copy), ctx, projectName, options) } // Create mocks base method. -func (m *MockService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error { +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) @@ -106,13 +107,13 @@ func (m *MockService) Create(ctx context.Context, project *types.Project, option } // Create indicates an expected call of Create. -func (mr *MockServiceMockRecorder) Create(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Create), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCompose)(nil).Create), ctx, project, options) } // Down mocks base method. -func (m *MockService) Down(ctx context.Context, projectName string, options api.DownOptions) error { +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) @@ -120,28 +121,13 @@ func (m *MockService) Down(ctx context.Context, projectName string, options api. } // Down indicates an expected call of Down. -func (mr *MockServiceMockRecorder) Down(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Down), ctx, projectName, options) -} - -// DryRunMode mocks base method. -func (m *MockService) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DryRunMode", ctx, dryRun) - ret0, _ := ret[0].(context.Context) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DryRunMode indicates an expected call of DryRunMode. -func (mr *MockServiceMockRecorder) DryRunMode(ctx, dryRun any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DryRunMode", reflect.TypeOf((*MockService)(nil).DryRunMode), ctx, dryRun) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Down", reflect.TypeOf((*MockCompose)(nil).Down), ctx, projectName, options) } // Events mocks base method. -func (m *MockService) Events(ctx context.Context, projectName string, options api.EventsOptions) error { +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) @@ -149,13 +135,13 @@ func (m *MockService) Events(ctx context.Context, projectName string, options ap } // Events indicates an expected call of Events. -func (mr *MockServiceMockRecorder) Events(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Events), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockCompose)(nil).Events), ctx, projectName, options) } // Exec mocks base method. -func (m *MockService) Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) { +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) @@ -164,13 +150,13 @@ func (m *MockService) Exec(ctx context.Context, projectName string, options api. } // Exec indicates an expected call of Exec. -func (mr *MockServiceMockRecorder) Exec(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Exec), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCompose)(nil).Exec), ctx, projectName, options) } // Export mocks base method. -func (m *MockService) Export(ctx context.Context, projectName string, options api.ExportOptions) error { +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) @@ -178,13 +164,13 @@ func (m *MockService) Export(ctx context.Context, projectName string, options ap } // Export indicates an expected call of Export. -func (mr *MockServiceMockRecorder) Export(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Export), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockCompose)(nil).Export), ctx, projectName, options) } // Generate mocks base method. -func (m *MockService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) { +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) @@ -193,13 +179,29 @@ func (m *MockService) Generate(ctx context.Context, options api.GenerateOptions) } // Generate indicates an expected call of Generate. -func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call { +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) +} + +// GetConfiguredStreams mocks base method. +func (m *MockCompose) GetConfiguredStreams() (io.Writer, io.Writer, io.Reader) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfiguredStreams") + ret0, _ := ret[0].(io.Writer) + ret1, _ := ret[1].(io.Writer) + ret2, _ := ret[2].(io.Reader) + return ret0, ret1, ret2 +} + +// GetConfiguredStreams indicates an expected call of GetConfiguredStreams. +func (mr *MockComposeMockRecorder) GetConfiguredStreams() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockService)(nil).Generate), ctx, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfiguredStreams", reflect.TypeOf((*MockCompose)(nil).GetConfiguredStreams)) } // Images mocks base method. -func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) { +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) @@ -208,13 +210,13 @@ func (m *MockService) Images(ctx context.Context, projectName string, options ap } // Images indicates an expected call of Images. -func (mr *MockServiceMockRecorder) Images(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Images), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Images", reflect.TypeOf((*MockCompose)(nil).Images), ctx, projectName, options) } // Kill mocks base method. -func (m *MockService) Kill(ctx context.Context, projectName string, options api.KillOptions) error { +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) @@ -222,13 +224,13 @@ func (m *MockService) Kill(ctx context.Context, projectName string, options api. } // Kill indicates an expected call of Kill. -func (mr *MockServiceMockRecorder) Kill(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Kill), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kill", reflect.TypeOf((*MockCompose)(nil).Kill), ctx, projectName, options) } // List mocks base method. -func (m *MockService) List(ctx context.Context, options api.ListOptions) ([]api.Stack, error) { +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) @@ -237,39 +239,42 @@ func (m *MockService) List(ctx context.Context, options api.ListOptions) ([]api. } // List indicates an expected call of List. -func (mr *MockServiceMockRecorder) List(ctx, options any) *gomock.Call { +func (mr *MockComposeMockRecorder) List(ctx, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockService)(nil).List), ctx, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCompose)(nil).List), ctx, options) } -// Logs mocks base method. -func (m *MockService) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error { +// 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, "Logs", ctx, projectName, consumer, options) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "LoadProject", ctx, options) + ret0, _ := ret[0].(*types.Project) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// Logs indicates an expected call of Logs. -func (mr *MockServiceMockRecorder) Logs(ctx, projectName, consumer, options any) *gomock.Call { +// 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, "Logs", reflect.TypeOf((*MockService)(nil).Logs), ctx, projectName, consumer, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadProject", reflect.TypeOf((*MockCompose)(nil).LoadProject), ctx, options) } -// MaxConcurrency mocks base method. -func (m *MockService) MaxConcurrency(parallel int) { +// Logs mocks base method. +func (m *MockCompose) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error { m.ctrl.T.Helper() - m.ctrl.Call(m, "MaxConcurrency", parallel) + ret := m.ctrl.Call(m, "Logs", ctx, projectName, consumer, options) + ret0, _ := ret[0].(error) + return ret0 } -// MaxConcurrency indicates an expected call of MaxConcurrency. -func (mr *MockServiceMockRecorder) MaxConcurrency(parallel any) *gomock.Call { +// 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, "MaxConcurrency", reflect.TypeOf((*MockService)(nil).MaxConcurrency), parallel) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockCompose)(nil).Logs), ctx, projectName, consumer, options) } // Pause mocks base method. -func (m *MockService) Pause(ctx context.Context, projectName string, options api.PauseOptions) error { +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) @@ -277,13 +282,13 @@ func (m *MockService) Pause(ctx context.Context, projectName string, options api } // Pause indicates an expected call of Pause. -func (mr *MockServiceMockRecorder) Pause(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Pause), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockCompose)(nil).Pause), ctx, projectName, options) } // Port mocks base method. -func (m *MockService) Port(ctx context.Context, projectName, service string, port uint16, options api.PortOptions) (string, int, error) { +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) @@ -293,13 +298,13 @@ func (m *MockService) Port(ctx context.Context, projectName, service string, por } // Port indicates an expected call of Port. -func (mr *MockServiceMockRecorder) Port(ctx, projectName, service, port, options any) *gomock.Call { +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((*MockService)(nil).Port), ctx, projectName, service, port, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Port", reflect.TypeOf((*MockCompose)(nil).Port), ctx, projectName, service, port, options) } // Ps mocks base method. -func (m *MockService) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) { +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) @@ -308,13 +313,13 @@ func (m *MockService) Ps(ctx context.Context, projectName string, options api.Ps } // Ps indicates an expected call of Ps. -func (mr *MockServiceMockRecorder) Ps(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Ps), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ps", reflect.TypeOf((*MockCompose)(nil).Ps), ctx, projectName, options) } // Publish mocks base method. -func (m *MockService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { +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) @@ -322,13 +327,13 @@ func (m *MockService) Publish(ctx context.Context, project *types.Project, repos } // Publish indicates an expected call of Publish. -func (mr *MockServiceMockRecorder) Publish(ctx, project, repository, options any) *gomock.Call { +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((*MockService)(nil).Publish), ctx, project, repository, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockCompose)(nil).Publish), ctx, project, repository, options) } // Pull mocks base method. -func (m *MockService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error { +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) @@ -336,13 +341,13 @@ func (m *MockService) Pull(ctx context.Context, project *types.Project, options } // Pull indicates an expected call of Pull. -func (mr *MockServiceMockRecorder) Pull(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Pull), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pull", reflect.TypeOf((*MockCompose)(nil).Pull), ctx, project, options) } // Push mocks base method. -func (m *MockService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error { +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) @@ -350,13 +355,13 @@ func (m *MockService) Push(ctx context.Context, project *types.Project, options } // Push indicates an expected call of Push. -func (mr *MockServiceMockRecorder) Push(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Push), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockCompose)(nil).Push), ctx, project, options) } // Remove mocks base method. -func (m *MockService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error { +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) @@ -364,13 +369,13 @@ func (m *MockService) Remove(ctx context.Context, projectName string, options ap } // Remove indicates an expected call of Remove. -func (mr *MockServiceMockRecorder) Remove(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Remove), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCompose)(nil).Remove), ctx, projectName, options) } // Restart mocks base method. -func (m *MockService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error { +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) @@ -378,13 +383,13 @@ func (m *MockService) Restart(ctx context.Context, projectName string, options a } // Restart indicates an expected call of Restart. -func (mr *MockServiceMockRecorder) Restart(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Restart), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restart", reflect.TypeOf((*MockCompose)(nil).Restart), ctx, projectName, options) } // RunOneOffContainer mocks base method. -func (m *MockService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) { +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) @@ -393,13 +398,13 @@ func (m *MockService) RunOneOffContainer(ctx context.Context, project *types.Pro } // RunOneOffContainer indicates an expected call of RunOneOffContainer. -func (mr *MockServiceMockRecorder) RunOneOffContainer(ctx, project, opts any) *gomock.Call { +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((*MockService)(nil).RunOneOffContainer), ctx, project, opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOneOffContainer", reflect.TypeOf((*MockCompose)(nil).RunOneOffContainer), ctx, project, opts) } // Scale mocks base method. -func (m *MockService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error { +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) @@ -407,13 +412,13 @@ func (m *MockService) Scale(ctx context.Context, project *types.Project, options } // Scale indicates an expected call of Scale. -func (mr *MockServiceMockRecorder) Scale(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Scale), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scale", reflect.TypeOf((*MockCompose)(nil).Scale), ctx, project, options) } // Start mocks base method. -func (m *MockService) Start(ctx context.Context, projectName string, options api.StartOptions) error { +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) @@ -421,13 +426,13 @@ func (m *MockService) Start(ctx context.Context, projectName string, options api } // Start indicates an expected call of Start. -func (mr *MockServiceMockRecorder) Start(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Start), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockCompose)(nil).Start), ctx, projectName, options) } // Stop mocks base method. -func (m *MockService) Stop(ctx context.Context, projectName string, options api.StopOptions) error { +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) @@ -435,13 +440,13 @@ func (m *MockService) Stop(ctx context.Context, projectName string, options api. } // Stop indicates an expected call of Stop. -func (mr *MockServiceMockRecorder) Stop(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Stop), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockCompose)(nil).Stop), ctx, projectName, options) } // Top mocks base method. -func (m *MockService) Top(ctx context.Context, projectName string, services []string) ([]api.ContainerProcSummary, error) { +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) @@ -450,13 +455,13 @@ func (m *MockService) Top(ctx context.Context, projectName string, services []st } // Top indicates an expected call of Top. -func (mr *MockServiceMockRecorder) Top(ctx, projectName, services any) *gomock.Call { +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((*MockService)(nil).Top), ctx, projectName, services) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Top", reflect.TypeOf((*MockCompose)(nil).Top), ctx, projectName, services) } // UnPause mocks base method. -func (m *MockService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error { +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) @@ -464,13 +469,13 @@ func (m *MockService) UnPause(ctx context.Context, projectName string, options a } // UnPause indicates an expected call of UnPause. -func (mr *MockServiceMockRecorder) UnPause(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).UnPause), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnPause", reflect.TypeOf((*MockCompose)(nil).UnPause), ctx, projectName, options) } // Up mocks base method. -func (m *MockService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { +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) @@ -478,13 +483,13 @@ func (m *MockService) Up(ctx context.Context, project *types.Project, options ap } // Up indicates an expected call of Up. -func (mr *MockServiceMockRecorder) Up(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Up), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*MockCompose)(nil).Up), ctx, project, options) } // Viz mocks base method. -func (m *MockService) Viz(ctx context.Context, project *types.Project, options api.VizOptions) (string, error) { +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) @@ -493,13 +498,13 @@ func (m *MockService) Viz(ctx context.Context, project *types.Project, options a } // Viz indicates an expected call of Viz. -func (mr *MockServiceMockRecorder) Viz(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Viz), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockCompose)(nil).Viz), ctx, project, options) } // Volumes mocks base method. -func (m *MockService) Volumes(ctx context.Context, project string, options api.VolumesOptions) ([]api.VolumesSummary, error) { +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) @@ -508,13 +513,13 @@ func (m *MockService) Volumes(ctx context.Context, project string, options api.V } // Volumes indicates an expected call of Volumes. -func (mr *MockServiceMockRecorder) Volumes(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Volumes), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Volumes", reflect.TypeOf((*MockCompose)(nil).Volumes), ctx, project, options) } // Wait mocks base method. -func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) { +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) @@ -523,13 +528,13 @@ func (m *MockService) Wait(ctx context.Context, projectName string, options api. } // Wait indicates an expected call of Wait. -func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options any) *gomock.Call { +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((*MockService)(nil).Wait), ctx, projectName, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockCompose)(nil).Wait), ctx, projectName, options) } // Watch mocks base method. -func (m *MockService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error { +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) @@ -537,9 +542,9 @@ func (m *MockService) Watch(ctx context.Context, project *types.Project, options } // Watch indicates an expected call of Watch. -func (mr *MockServiceMockRecorder) Watch(ctx, project, options any) *gomock.Call { +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((*MockService)(nil).Watch), ctx, project, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockCompose)(nil).Watch), ctx, project, options) } // MockLogConsumer is a mock of LogConsumer interface. From 198b35797e8a5113c89a5fcfa9fb39cda98c2b51 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:34:25 +0100 Subject: [PATCH 2/2] Migrate CLI commands to use LoadProject API Simplifying the codebase and eliminating duplicate backend creation. Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- cmd/compose/bridge.go | 8 +++- cmd/compose/build.go | 11 ++--- cmd/compose/completion.go | 14 ++++++- cmd/compose/compose.go | 87 ++++++++++++++++----------------------- cmd/compose/config.go | 81 +++++++++++++++++++++++++++++------- cmd/compose/publish.go | 18 ++++---- cmd/compose/pull.go | 7 ++-- cmd/compose/push.go | 11 ++--- cmd/compose/run.go | 19 +++++---- cmd/compose/scale.go | 11 ++--- cmd/compose/viz.go | 8 ++-- cmd/compose/watch.go | 15 +++---- 12 files changed, 173 insertions(+), 117 deletions(-) diff --git a/cmd/compose/bridge.go b/cmd/compose/bridge.go index 22f78b68998..bd0d11b2a29 100644 --- a/cmd/compose/bridge.go +++ b/cmd/compose/bridge.go @@ -30,6 +30,7 @@ import ( "github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/pkg/bridge" + "github.com/docker/compose/v2/pkg/compose" ) func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { @@ -62,7 +63,12 @@ func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { } func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error { - project, _, err := p.ToProject(ctx, dockerCli, nil) + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return err + } + + project, _, err := p.ToProject(ctx, dockerCli, backend, nil) if err != nil { return err } diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 7307efb4392..0212b6dddd1 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -150,8 +150,13 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back } func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error { + 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, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution) + project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -166,9 +171,5 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *Backen } apiBuildOptions.Attestations = true - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } return backend.Build(ctx, project, apiBuildOptions) } diff --git a/cmd/compose/completion.go b/cmd/compose/completion.go index 59bfdd885a0..9f206bde5bc 100644 --- a/cmd/compose/completion.go +++ b/cmd/compose/completion.go @@ -38,7 +38,12 @@ func noCompletion() validArgsFn { func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { p.Offline = true - project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) + 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 } @@ -79,7 +84,12 @@ func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions) func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { p.Offline = true - project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) + 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 } diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 7ebaa0cac51..bcafe158346 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -159,12 +159,12 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func // 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, args []string) error { - options := []cli.ProjectOptionsFn{ - cli.WithResolvedPaths(true), - cli.WithoutEnvironmentResolution, + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return err } - project, metrics, err := o.ToProject(ctx, dockerCli, args, options...) + project, metrics, err := o.ToProject(ctx, dockerCli, backend, args, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -236,7 +236,12 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl name := o.ProjectName var project *types.Project if len(o.ConfigPaths) > 0 || o.ProjectName == "" { - p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution) + 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 != "" { @@ -260,7 +265,12 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl return envProjectName, nil } - project, _, err := o.ToProject(ctx, dockerCli, nil) + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return "", err + } + + project, _, err := o.ToProject(ctx, dockerCli, backend, nil) if err != nil { return "", err } @@ -285,19 +295,14 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser return options.LoadModel(ctx) } -func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo +// 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) - for _, r := range remotes { - po = append(po, cli.WithResourceLoader(r)) - } - - options, err := o.toProjectOptions(po...) - if err != nil { - return nil, metrics, err - } - options.WithListeners(func(event string, metadata map[string]any) { + // Setup metrics listener to collect project data + metricsListener := func(event string, metadata map[string]any) { switch event { case "extends": metrics.CountExtends++ @@ -318,50 +323,28 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s } } } - }) - - if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) { - api.Separator = "_" - } - - project, err := options.LoadProject(ctx) - if err != nil { - return nil, metrics, err } - if project.Name == "" { - return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name") + 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}, } - project, err = project.WithServicesEnabled(services...) + project, err := backend.LoadProject(ctx, loadOpts) if err != nil { return nil, metrics, err } - 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", // default, will be overridden by `run` command - } - if len(o.EnvFiles) != 0 { - s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",") - } - project.Services[name] = s - } - - project, err = project.WithSelectedServices(services) - if err != nil { - return nil, tracing.Metrics{}, err - } - - if !o.All { - project = project.WithoutUnnecessaryResources() - } - return project, metrics, err + return project, metrics, nil } func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader { diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 5edcb3db985..795d73c186e 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -61,19 +61,19 @@ type configOptions struct { lockImageDigests bool } -func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { - po = append(po, o.ToProjectOptions()...) - project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...) +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.ToProjectOptions()...) + po = append(po, o.toProjectOptionsFns()...) return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...) } -func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn { - return []cli.ProjectOptionsFn{ +// 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), @@ -81,6 +81,10 @@ func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn { 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 { @@ -197,7 +201,12 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s } func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) { - project, err := opts.ToProject(ctx, dockerCli, services) + 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 } @@ -353,7 +362,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) return nil } - project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + 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 } @@ -366,7 +380,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) } func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error { - project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + 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 } @@ -377,7 +396,12 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) } func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error { - project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + 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 } @@ -388,7 +412,12 @@ func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) } func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error { - project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + 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 } @@ -405,7 +434,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err if opts.hash != "*" { services = append(services, strings.Split(opts.hash, ",")...) } - project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + + 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 } @@ -440,7 +475,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { set := map[string]struct{}{} - project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return err + } + + project, err := opts.ToProject(ctx, dockerCli, backend, services) if err != nil { return err } @@ -461,7 +502,12 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, } func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { - project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return err + } + + project, err := opts.ToProject(ctx, dockerCli, backend, services) if err != nil { return err } @@ -498,7 +544,12 @@ func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions } func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { - project, err := opts.ToProject(ctx, dockerCli, services) + backend, err := compose.NewComposeService(dockerCli) + if err != nil { + return err + } + + project, err := opts.ToProject(ctx, dockerCli, backend, services) if err != nil { return err } diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index becd162eeff..86d526f31b8 100644 --- a/cmd/compose/publish.go +++ b/cmd/compose/publish.go @@ -69,7 +69,16 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Ba } func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error { - project, metrics, err := opts.ToProject(ctx, dockerCli, nil) + 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 } @@ -78,13 +87,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *Back return errors.New("cannot publish compose file with local includes") } - if opts.assumeYes { - backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt())) - } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } return backend.Publish(ctx, project, repository, api.PublishOptions{ ResolveImageDigests: opts.resolveImageDigests || opts.app, Application: opts.app, diff --git a/cmd/compose/pull.go b/cmd/compose/pull.go index 9cf5e5d2809..4e4e58b4886 100644 --- a/cmd/compose/pull.go +++ b/cmd/compose/pull.go @@ -99,20 +99,21 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types } func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error { - project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) if err != nil { return err } - project, err = opts.apply(project, services) + project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution) if err != nil { return err } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) + project, err = opts.apply(project, services) if err != nil { return err } + return backend.Pull(ctx, project, api.PullOptions{ Quiet: opts.quiet, IgnoreFailures: opts.ignorePullFailures, diff --git a/cmd/compose/push.go b/cmd/compose/push.go index bbb4152dac7..c76d1eca041 100644 --- a/cmd/compose/push.go +++ b/cmd/compose/push.go @@ -55,7 +55,12 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe } func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error { - project, _, err := opts.ToProject(ctx, dockerCli, services) + 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 } @@ -67,10 +72,6 @@ func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *Backend } } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } return backend.Push(ctx, project, api.PushOptions{ IgnoreFailures: opts.Ignorefailures, Quiet: opts.Quiet, diff --git a/cmd/compose/run.go b/cmd/compose/run.go index 0894a8cbaed..ab528837b6a 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -22,6 +22,7 @@ import ( "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/docker/compose/v2/pkg/compose" @@ -29,7 +30,6 @@ import ( xprogress "github.com/moby/buildkit/util/progress/progressui" "github.com/sirupsen/logrus" - cgo "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" @@ -143,7 +143,7 @@ func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (t return environment, nil } -func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { +func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo options := runOptions{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -204,7 +204,12 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution) + 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 } @@ -219,7 +224,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen } options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans]) - return runRun(ctx, backendOptions, project, options, createOpts, buildOpts, dockerCli) + return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli) }), ValidArgsFunction: completeServiceNames(dockerCli, p), } @@ -267,7 +272,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { return pflag.NormalizedName(name) } -func runRun(ctx context.Context, backendOptions *BackendOptions, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error { +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 @@ -339,10 +344,6 @@ func runRun(ctx context.Context, backendOptions *BackendOptions, project *types. } } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts) if exitCode != 0 { errMsg := "" diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go index 3f6c492726c..df9030ae0ff 100644 --- a/cmd/compose/scale.go +++ b/cmd/compose/scale.go @@ -60,8 +60,13 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back } 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, services) + project, _, err := opts.ToProject(ctx, dockerCli, backend, services) if err != nil { return err } @@ -81,10 +86,6 @@ func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *Backen project.Services[key] = service } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } return backend.Scale(ctx, project, api.ScaleOptions{Services: services}) } diff --git a/cmd/compose/viz.go b/cmd/compose/viz.go index 4bf0a52812b..f55d12f3fc9 100644 --- a/cmd/compose/viz.go +++ b/cmd/compose/viz.go @@ -66,16 +66,18 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen func runViz(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *vizOptions) error { _, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL") - project, _, err := opts.ToProject(ctx, dockerCli, nil) + + backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) if err != nil { return err } - // build graph - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) + 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, diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go index 68d9004e0dc..70648fcdaf5 100644 --- a/cmd/compose/watch.go +++ b/cmd/compose/watch.go @@ -66,7 +66,12 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back } func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error { - project, _, err := watchOpts.ToProject(ctx, dockerCli, services) + 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 } @@ -112,19 +117,11 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *Backen Services: services, }, } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } if err := backend.Up(ctx, project, upOpts); err != nil { return err } } - backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) - if err != nil { - return err - } outStream, errStream, _ := backend.GetConfiguredStreams() consumer := formatter.NewLogConsumer(ctx, outStream, errStream, false, false, false) return backend.Watch(ctx, project, api.WatchOptions{