瀏覽代碼

Compose as a cli plugin

Signed-off-by: Guillaume Tardif <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
Guillaume Tardif 4 年之前
父節點
當前提交
1dc97e8c4b
共有 11 個文件被更改,包括 494 次插入97 次删除
  1. 143 0
      api/compose/delegator.go
  2. 143 0
      api/compose/noimpl.go
  3. 4 0
      builder.Makefile
  4. 8 0
      cli/cmd/compose/compose.go
  5. 35 0
      cli/config/flags.go
  6. 61 0
      cli/config/flags_test.go
  7. 4 58
      cli/main.go
  8. 0 36
      cli/main_test.go
  9. 28 0
      local/backend.go
  10. 58 0
      main.go
  11. 10 3
      utils/e2e/framework.go

+ 143 - 0
api/compose/delegator.go

@@ -0,0 +1,143 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"context"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+// ServiceDelegator implements Service by delegating to another implementation. This allows lazy init
+type ServiceDelegator struct {
+	Delegate Service
+}
+
+//Build implements Service interface
+func (s *ServiceDelegator) Build(ctx context.Context, project *types.Project, options BuildOptions) error {
+	return s.Delegate.Build(ctx, project, options)
+}
+
+//Push implements Service interface
+func (s *ServiceDelegator) Push(ctx context.Context, project *types.Project, options PushOptions) error {
+	return s.Delegate.Push(ctx, project, options)
+}
+
+//Pull implements Service interface
+func (s *ServiceDelegator) Pull(ctx context.Context, project *types.Project, options PullOptions) error {
+	return s.Delegate.Pull(ctx, project, options)
+}
+
+//Create implements Service interface
+func (s *ServiceDelegator) Create(ctx context.Context, project *types.Project, options CreateOptions) error {
+	return s.Delegate.Create(ctx, project, options)
+}
+
+//Start implements Service interface
+func (s *ServiceDelegator) Start(ctx context.Context, project *types.Project, options StartOptions) error {
+	return s.Delegate.Start(ctx, project, options)
+}
+
+//Restart implements Service interface
+func (s *ServiceDelegator) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
+	return s.Delegate.Restart(ctx, project, options)
+}
+
+//Stop implements Service interface
+func (s *ServiceDelegator) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
+	return s.Delegate.Stop(ctx, project, options)
+}
+
+//Up implements Service interface
+func (s *ServiceDelegator) Up(ctx context.Context, project *types.Project, options UpOptions) error {
+	return s.Delegate.Up(ctx, project, options)
+}
+
+//Down implements Service interface
+func (s *ServiceDelegator) Down(ctx context.Context, project string, options DownOptions) error {
+	return s.Delegate.Down(ctx, project, options)
+}
+
+//Logs implements Service interface
+func (s *ServiceDelegator) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error {
+	return s.Delegate.Logs(ctx, project, consumer, options)
+}
+
+//Ps implements Service interface
+func (s *ServiceDelegator) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) {
+	return s.Delegate.Ps(ctx, project, options)
+}
+
+//List implements Service interface
+func (s *ServiceDelegator) List(ctx context.Context, options ListOptions) ([]Stack, error) {
+	return s.Delegate.List(ctx, options)
+}
+
+//Convert implements Service interface
+func (s *ServiceDelegator) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) {
+	return s.Delegate.Convert(ctx, project, options)
+}
+
+//Kill implements Service interface
+func (s *ServiceDelegator) Kill(ctx context.Context, project *types.Project, options KillOptions) error {
+	return s.Delegate.Kill(ctx, project, options)
+}
+
+//RunOneOffContainer implements Service interface
+func (s *ServiceDelegator) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) {
+	return s.Delegate.RunOneOffContainer(ctx, project, options)
+}
+
+//Remove implements Service interface
+func (s *ServiceDelegator) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) {
+	return s.Delegate.Remove(ctx, project, options)
+}
+
+//Exec implements Service interface
+func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, options RunOptions) error {
+	return s.Delegate.Exec(ctx, project, options)
+}
+
+//Pause implements Service interface
+func (s *ServiceDelegator) Pause(ctx context.Context, project string, options PauseOptions) error {
+	return s.Delegate.Pause(ctx, project, options)
+}
+
+//UnPause implements Service interface
+func (s *ServiceDelegator) UnPause(ctx context.Context, project string, options PauseOptions) error {
+	return s.Delegate.UnPause(ctx, project, options)
+}
+
+//Top implements Service interface
+func (s *ServiceDelegator) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) {
+	return s.Delegate.Top(ctx, project, services)
+}
+
+//Events implements Service interface
+func (s *ServiceDelegator) Events(ctx context.Context, project string, options EventsOptions) error {
+	return s.Delegate.Events(ctx, project, options)
+}
+
+//Port implements Service interface
+func (s *ServiceDelegator) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) {
+	return s.Delegate.Port(ctx, project, service, port, options)
+}
+
+//Images implements Service interface
+func (s *ServiceDelegator) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
+	return s.Delegate.Images(ctx, project, options)
+}

+ 143 - 0
api/compose/noimpl.go

@@ -0,0 +1,143 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"context"
+
+	"github.com/compose-spec/compose-go/types"
+
+	"github.com/docker/compose-cli/api/errdefs"
+)
+
+// NoImpl implements Service to return ErrNotImplemented
+type NoImpl struct{}
+
+//Build implements Service interface
+func (s NoImpl) Build(ctx context.Context, project *types.Project, options BuildOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Push implements Service interface
+func (s NoImpl) Push(ctx context.Context, project *types.Project, options PushOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Pull implements Service interface
+func (s NoImpl) Pull(ctx context.Context, project *types.Project, options PullOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Create implements Service interface
+func (s NoImpl) Create(ctx context.Context, project *types.Project, options CreateOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Start implements Service interface
+func (s NoImpl) Start(ctx context.Context, project *types.Project, options StartOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Restart implements Service interface
+func (s NoImpl) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Stop implements Service interface
+func (s NoImpl) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Up implements Service interface
+func (s NoImpl) Up(ctx context.Context, project *types.Project, options UpOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Down implements Service interface
+func (s NoImpl) Down(ctx context.Context, project string, options DownOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Logs implements Service interface
+func (s NoImpl) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Ps implements Service interface
+func (s NoImpl) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//List implements Service interface
+func (s NoImpl) List(ctx context.Context, options ListOptions) ([]Stack, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Convert implements Service interface
+func (s NoImpl) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Kill implements Service interface
+func (s NoImpl) Kill(ctx context.Context, project *types.Project, options KillOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//RunOneOffContainer implements Service interface
+func (s NoImpl) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) {
+	return 0, errdefs.ErrNotImplemented
+}
+
+//Remove implements Service interface
+func (s NoImpl) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Exec implements Service interface
+func (s NoImpl) Exec(ctx context.Context, project *types.Project, options RunOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Pause implements Service interface
+func (s NoImpl) Pause(ctx context.Context, project string, options PauseOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//UnPause implements Service interface
+func (s NoImpl) UnPause(ctx context.Context, project string, options PauseOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Top implements Service interface
+func (s NoImpl) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Events implements Service interface
+func (s NoImpl) Events(ctx context.Context, project string, options EventsOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Port implements Service interface
+func (s NoImpl) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) {
+	return "", 0, errdefs.ErrNotImplemented
+}
+
+//Images implements Service interface
+func (s NoImpl) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 4 - 0
builder.Makefile

@@ -57,6 +57,10 @@ protos:
 cli:
 	GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(BINARY_WITH_EXTENSION) ./cli
 
+.PHONY: compose-plugin
+compose-plugin:
+	GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o ./bin/docker-compose .
+
 .PHONY: cross
 cross:
 	GOOS=linux   GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-amd64 ./cli

+ 8 - 0
cli/cmd/compose/compose.go

@@ -123,6 +123,14 @@ func Command(contextType string, backend compose.Service) *cobra.Command {
 			return fmt.Errorf("unknown docker command: %q", "compose "+args[0])
 		},
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			parent := cmd.Root()
+			parentPrerun := parent.PersistentPreRunE
+			if parentPrerun != nil {
+				err := parentPrerun(cmd, args)
+				if err != nil {
+					return err
+				}
+			}
 			if noAnsi {
 				if ansi != "auto" {
 					return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)

+ 35 - 0
cli/config/flags.go

@@ -17,9 +17,11 @@
 package config
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 
+	"github.com/pkg/errors"
 	"github.com/spf13/pflag"
 
 	"github.com/docker/compose-cli/api/config"
@@ -44,3 +46,36 @@ func confDir() string {
 	home, _ := os.UserHomeDir()
 	return filepath.Join(home, config.ConfigFileDir)
 }
+
+// GetCurrentContext get current context based on opts, env vars
+func GetCurrentContext(contextOpt string, configDir string, hosts []string) string {
+	// host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname
+	// -H flag disables context --> set default as current
+	if len(hosts) > 0 {
+		return "default"
+	}
+	// DOCKER_HOST disables context --> set default as current
+	if _, present := os.LookupEnv("DOCKER_HOST"); present {
+		return "default"
+	}
+	res := contextOpt
+	if res == "" {
+		// check if DOCKER_CONTEXT env variable was set
+		if _, present := os.LookupEnv("DOCKER_CONTEXT"); present {
+			res = os.Getenv("DOCKER_CONTEXT")
+		}
+
+		if res == "" {
+			config, err := config.LoadFile(configDir)
+			if err != nil {
+				fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING"))
+				return "default"
+			}
+			res = config.CurrentContext
+		}
+	}
+	if res == "" {
+		res = "default"
+	}
+	return res
+}

+ 61 - 0
cli/config/flags_test.go

@@ -0,0 +1,61 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package config
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gotest.tools/v3/assert"
+
+	"github.com/docker/compose-cli/api/config"
+)
+
+var contextSetConfig = []byte(`{
+	"currentContext": "some-context"
+}`)
+
+func TestDetermineCurrentContext(t *testing.T) {
+	d, err := ioutil.TempDir("", "")
+	// nolint errcheck
+	defer os.RemoveAll(d)
+	assert.NilError(t, err)
+	err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644)
+	assert.NilError(t, err)
+
+	// If nothing set, fallback to default
+	c := GetCurrentContext("", "", []string{})
+	assert.Equal(t, c, "default")
+
+	// If context flag set, use that
+	c = GetCurrentContext("other-context", "", []string{})
+	assert.Equal(t, c, "other-context")
+
+	// If no context flag, use config
+	c = GetCurrentContext("", d, []string{})
+	assert.Equal(t, c, "some-context")
+
+	// Ensure context flag overrides config
+	c = GetCurrentContext("other-context", d, []string{})
+	assert.Equal(t, "other-context", c)
+
+	// Ensure host flag overrides context
+	c = GetCurrentContext("other-context", d, []string{"hostname"})
+	assert.Equal(t, "default", c)
+}

+ 4 - 58
cli/main.go

@@ -29,9 +29,6 @@ import (
 	"time"
 
 	"github.com/docker/cli/cli"
-	"github.com/docker/cli/cli/command"
-	cliconfig "github.com/docker/cli/cli/config"
-	cliflags "github.com/docker/cli/cli/flags"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -48,6 +45,7 @@ import (
 	"github.com/docker/compose-cli/cli/cmd/logout"
 	"github.com/docker/compose-cli/cli/cmd/run"
 	"github.com/docker/compose-cli/cli/cmd/volume"
+	cliconfig "github.com/docker/compose-cli/cli/config"
 	"github.com/docker/compose-cli/cli/metrics"
 	"github.com/docker/compose-cli/cli/mobycli"
 	cliopts "github.com/docker/compose-cli/cli/options"
@@ -62,7 +60,6 @@ import (
 
 var (
 	contextAgnosticCommands = map[string]struct{}{
-		"compose":          {},
 		"context":          {},
 		"login":            {},
 		"logout":           {},
@@ -198,7 +195,7 @@ func main() {
 	configDir := opts.Config
 	config.WithDir(configDir)
 
-	currentContext := determineCurrentContext(opts.Context, configDir, opts.Hosts)
+	currentContext := cliconfig.GetCurrentContext(opts.Context, configDir, opts.Hosts)
 	apicontext.WithCurrentContext(currentContext)
 
 	s, err := store.New(configDir)
@@ -234,27 +231,7 @@ func main() {
 func getBackend(ctype string, configDir string, opts cliopts.GlobalOpts) (backend.Service, error) {
 	switch ctype {
 	case store.DefaultContextType, store.LocalContextType:
-		configFile, err := cliconfig.Load(configDir)
-		if err != nil {
-			return nil, err
-		}
-		options := cliflags.CommonOptions{
-			Context:  opts.Context,
-			Debug:    opts.Debug,
-			Hosts:    opts.Hosts,
-			LogLevel: opts.LogLevel,
-		}
-
-		if opts.TLSVerify {
-			options.TLS = opts.TLS
-			options.TLSVerify = opts.TLSVerify
-			options.TLSOptions = opts.TLSOptions
-		}
-		apiClient, err := command.NewAPIClientFromFlags(&options, configFile)
-		if err != nil {
-			return nil, err
-		}
-		return local.NewService(apiClient), nil
+		return local.GetLocalBackend(configDir, opts)
 	}
 	service, err := backend.Get(ctype)
 	if errdefs.IsNotFoundError(err) {
@@ -311,6 +288,7 @@ func exit(ctx string, err error, ctype string) {
 	}
 
 	if compose.Warning != "" {
+		logrus.Warn(err)
 		fmt.Fprintln(os.Stderr, compose.Warning)
 	}
 
@@ -354,38 +332,6 @@ func newSigContext() (context.Context, func()) {
 	return ctx, cancel
 }
 
-func determineCurrentContext(flag string, configDir string, hosts []string) string {
-	// host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname
-	// -H flag disables context --> set default as current
-	if len(hosts) > 0 {
-		return "default"
-	}
-	// DOCKER_HOST disables context --> set default as current
-	if _, present := os.LookupEnv("DOCKER_HOST"); present {
-		return "default"
-	}
-	res := flag
-	if res == "" {
-		// check if DOCKER_CONTEXT env variable was set
-		if _, present := os.LookupEnv("DOCKER_CONTEXT"); present {
-			res = os.Getenv("DOCKER_CONTEXT")
-		}
-
-		if res == "" {
-			config, err := config.LoadFile(configDir)
-			if err != nil {
-				fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING"))
-				return "default"
-			}
-			res = config.CurrentContext
-		}
-	}
-	if res == "" {
-		res = "default"
-	}
-	return res
-}
-
 func walk(c *cobra.Command, f func(*cobra.Command)) {
 	f(c)
 	for _, c := range c.Commands() {

+ 0 - 36
cli/main_test.go

@@ -17,53 +17,17 @@
 package main
 
 import (
-	"io/ioutil"
 	"os"
-	"path/filepath"
 	"testing"
 
 	"gotest.tools/v3/assert"
 
-	"github.com/docker/compose-cli/api/config"
 	"github.com/docker/compose-cli/cli/cmd"
 	"github.com/docker/compose-cli/cli/cmd/context"
 	"github.com/docker/compose-cli/cli/cmd/login"
 	"github.com/docker/compose-cli/cli/cmd/run"
 )
 
-var contextSetConfig = []byte(`{
-	"currentContext": "some-context"
-}`)
-
-func TestDetermineCurrentContext(t *testing.T) {
-	d, err := ioutil.TempDir("", "")
-	// nolint errcheck
-	defer os.RemoveAll(d)
-	assert.NilError(t, err)
-	err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644)
-	assert.NilError(t, err)
-
-	// If nothing set, fallback to default
-	c := determineCurrentContext("", "", []string{})
-	assert.Equal(t, c, "default")
-
-	// If context flag set, use that
-	c = determineCurrentContext("other-context", "", []string{})
-	assert.Equal(t, c, "other-context")
-
-	// If no context flag, use config
-	c = determineCurrentContext("", d, []string{})
-	assert.Equal(t, c, "some-context")
-
-	// Ensure context flag overrides config
-	c = determineCurrentContext("other-context", d, []string{})
-	assert.Equal(t, "other-context", c)
-
-	// Ensure host flag overrides context
-	c = determineCurrentContext("other-context", d, []string{"hostname"})
-	assert.Equal(t, "default", c)
-}
-
 func TestCheckOwnCommand(t *testing.T) {
 	assert.Assert(t, isContextAgnosticCommand(login.Command()))
 	assert.Assert(t, isContextAgnosticCommand(context.Command()))

+ 28 - 0
local/backend.go

@@ -19,7 +19,9 @@ package local
 import (
 	"os"
 
+	"github.com/docker/cli/cli/command"
 	cliconfig "github.com/docker/cli/cli/config"
+	cliflags "github.com/docker/cli/cli/flags"
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/backend"
@@ -28,6 +30,7 @@ import (
 	"github.com/docker/compose-cli/api/resources"
 	"github.com/docker/compose-cli/api/secrets"
 	"github.com/docker/compose-cli/api/volumes"
+	cliopts "github.com/docker/compose-cli/cli/options"
 	local_compose "github.com/docker/compose-cli/local/compose"
 )
 
@@ -47,6 +50,31 @@ func NewService(apiClient client.APIClient) backend.Service {
 	}
 }
 
+// GetLocalBackend initialize local backend
+func GetLocalBackend(configDir string, opts cliopts.GlobalOpts) (backend.Service, error) {
+	configFile, err := cliconfig.Load(configDir)
+	if err != nil {
+		return nil, err
+	}
+	options := cliflags.CommonOptions{
+		Context:  opts.Context,
+		Debug:    opts.Debug,
+		Hosts:    opts.Hosts,
+		LogLevel: opts.LogLevel,
+	}
+
+	if opts.TLSVerify {
+		options.TLS = opts.TLS
+		options.TLSVerify = opts.TLSVerify
+		options.TLSOptions = opts.TLSOptions
+	}
+	apiClient, err := command.NewAPIClientFromFlags(&options, configFile)
+	if err != nil {
+		return nil, err
+	}
+	return NewService(apiClient), nil
+}
+
 func (s *local) ContainerService() containers.Service {
 	return s.containerService
 }

+ 58 - 0
main.go

@@ -0,0 +1,58 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package main
+
+import (
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/cli/cli-plugins/manager"
+	"github.com/docker/cli/cli-plugins/plugin"
+	"github.com/docker/cli/cli/command"
+	api "github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/context/store"
+	"github.com/docker/compose-cli/cli/cmd/compose"
+	"github.com/docker/compose-cli/internal"
+	impl "github.com/docker/compose-cli/local/compose"
+)
+
+func main() {
+	plugin.Run(func(dockerCli command.Cli) *cobra.Command {
+		lazyInit := api.ServiceDelegator{
+			Delegate: api.NoImpl{},
+		}
+		cmd := compose.Command(store.DefaultContextType, &lazyInit)
+		originalPreRun := cmd.PersistentPreRunE
+		cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+			if err := plugin.PersistentPreRunE(cmd, args); err != nil {
+				return err
+			}
+			lazyInit.Delegate = impl.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile())
+			if originalPreRun != nil {
+				return originalPreRun(cmd, args)
+			}
+			return nil
+		}
+		return cmd
+	},
+		manager.Metadata{
+			SchemaVersion: "0.1.0",
+			Vendor:        "Docker Inc.",
+			Version:       strings.TrimPrefix(internal.Version, "v"),
+		})
+}

+ 10 - 3
utils/e2e/framework.go

@@ -85,6 +85,13 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI {
 		_ = os.RemoveAll(d)
 	})
 
+	_ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755)
+	composePlugin, _ := findExecutable("docker-compose", []string{"../../bin", "../../../bin"})
+	err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", "docker-compose"))
+	if err != nil {
+		panic(err)
+	}
+
 	return &E2eCLI{binDir, d, t}
 }
 
@@ -117,7 +124,7 @@ func SetupExistingCLI() (string, func(), error) {
 		return "", nil, err
 	}
 
-	bin, err := findExecutable([]string{"../../bin", "../../../bin"})
+	bin, err := findExecutable(DockerExecutableName, []string{"../../bin", "../../../bin"})
 	if err != nil {
 		return "", nil, err
 	}
@@ -133,9 +140,9 @@ func SetupExistingCLI() (string, func(), error) {
 	return d, cleanup, nil
 }
 
-func findExecutable(paths []string) (string, error) {
+func findExecutable(executableName string, paths []string) (string, error) {
 	for _, p := range paths {
-		bin, err := filepath.Abs(path.Join(p, DockerExecutableName))
+		bin, err := filepath.Abs(path.Join(p, executableName))
 		if err != nil {
 			return "", err
 		}