Quellcode durchsuchen

Add `compose run` command

Signed-off-by: aiordache <[email protected]>
aiordache vor 5 Jahren
Ursprung
Commit
412385c495

+ 7 - 0
aci/compose.go

@@ -201,3 +201,10 @@ func (cs *aciComposeService) Logs(ctx context.Context, projectName string, consu
 func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }
+
+func (cs *aciComposeService) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	return "", errdefs.ErrNotImplemented
+}
+func (cs *aciComposeService) Run(ctx context.Context, container string, detach bool) error {
+	return errdefs.ErrNotImplemented
+}

+ 8 - 0
api/client/compose.go

@@ -71,3 +71,11 @@ func (c *composeService) List(context.Context, string) ([]compose.Stack, error)
 func (c *composeService) Convert(context.Context, *types.Project, compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }
+
+func (c *composeService) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	return "", errdefs.ErrNotImplemented
+}
+
+func (c *composeService) Run(ctx context.Context, container string, detach bool) error {
+	return errdefs.ErrNotImplemented
+}

+ 24 - 0
api/compose/api.go

@@ -18,6 +18,7 @@ package compose
 
 import (
 	"context"
+	"io"
 
 	"github.com/compose-spec/compose-go/types"
 )
@@ -46,6 +47,10 @@ type Service interface {
 	List(ctx context.Context, projectName string) ([]Stack, error)
 	// Convert translate compose model into backend's native format
 	Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
+	// CreateOneOffContainer creates a service oneoff container and starts its dependencies
+	CreateOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (string, error)
+	// Run attaches to and starts a one-off container
+	Run(ctx context.Context, container string, detach bool) error
 }
 
 // UpOptions group options of the Up API
@@ -66,6 +71,25 @@ type ConvertOptions struct {
 	Format string
 }
 
+// RunOptions holds all flags for compose run
+type RunOptions struct {
+	Name        string
+	Command     []string
+	WorkingDir  string
+	Environment []string
+	Publish     []string
+	Labels      []string
+	Volumes     []string
+	Remove      bool
+	NoDeps      bool
+	LogConsumer LogConsumer
+
+	Detach bool
+
+	Stdout io.ReadCloser
+	Stdin  io.WriteCloser
+}
+
 // PortPublisher hold status about published port
 type PortPublisher struct {
 	URL           string

+ 2 - 1
cli/cmd/compose/compose.go

@@ -90,6 +90,7 @@ func Command(contextType string) *cobra.Command {
 		listCommand(),
 		logsCommand(),
 		convertCommand(),
+		runCommand(),
 	)
 
 	if contextType == store.LocalContextType || contextType == store.DefaultContextType {
@@ -99,7 +100,7 @@ func Command(contextType string) *cobra.Command {
 			pullCommand(),
 		)
 	}
-
+	command.Flags().SetInterspersed(false)
 	return command
 }
 

+ 136 - 0
cli/cmd/compose/run.go

@@ -0,0 +1,136 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/compose-spec/compose-go/cli"
+	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/containers"
+	apicontext "github.com/docker/compose-cli/context"
+	"github.com/docker/compose-cli/context/store"
+	"github.com/docker/compose-cli/progress"
+)
+
+type runOptions struct {
+	Name        string
+	Command     []string
+	WorkingDir  string
+	Environment []string
+	Detach      bool
+	Publish     []string
+	Labels      []string
+	Volumes     []string
+	NoDeps      bool
+	Remove      bool
+}
+
+func runCommand() *cobra.Command {
+	opts := runOptions{}
+	runCmd := &cobra.Command{
+		Use:   "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]",
+		Short: "Run a one-off command on a service.",
+		Args:  cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			s := store.ContextStore(cmd.Context())
+			currentCtx, err := s.Get(apicontext.CurrentContext(cmd.Context()))
+			if err != nil {
+				return err
+			}
+			switch currentCtx.Type() {
+			case store.DefaultContextType:
+			default:
+				return fmt.Errorf(`Command "run" is not yet implemented for %q context type`, currentCtx.Type())
+			}
+
+			if len(args) > 1 {
+				opts.Command = args[1:]
+			}
+			opts.Name = args[0]
+			return runRun(cmd.Context(), opts)
+		},
+	}
+	runCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
+
+	runCmd.Flags().StringArrayVarP(&opts.Publish, "publish", "p", []string{}, "Publish a container's port(s). [HOST_PORT:]CONTAINER_PORT")
+	runCmd.Flags().StringVar(&opts.Name, "name", "", "Assign a name to the container")
+	runCmd.Flags().BoolVar(&opts.NoDeps, "no-deps", false, "Don't start linked services.")
+	runCmd.Flags().StringArrayVarP(&opts.Labels, "label", "l", []string{}, "Set meta data on a container")
+	runCmd.Flags().StringArrayVarP(&opts.Volumes, "volume", "v", []string{}, "Volume. Ex: storageaccount/my_share[:/absolute/path/to/target][:ro]")
+	runCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
+	runCmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
+	runCmd.Flags().BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
+
+	//addComposeCommonFlags(runCmd.Flags(), &opts.ComposeOpts)
+
+	runCmd.Flags().SetInterspersed(false)
+	return runCmd
+}
+
+func runRun(ctx context.Context, opts runOptions) error {
+	// target service
+	services := []string{opts.Name}
+
+	projectOpts := composeOptions{}
+	options, err := projectOpts.toProjectOptions()
+	if err != nil {
+		return err
+	}
+	project, err := cli.ProjectFromOptions(options)
+	if err != nil {
+		return err
+	}
+
+	err = filter(project, services)
+	if err != nil {
+		return err
+	}
+
+	c, err := client.NewWithDefaultLocalBackend(ctx)
+	if err != nil {
+		return err
+	}
+	containerID, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
+		return c.ComposeService().CreateOneOffContainer(ctx, project, compose.RunOptions{
+			Name:    opts.Name,
+			Command: opts.Command,
+		})
+	})
+	if err != nil {
+		return err
+	}
+	// start container and attach to container streams
+	err = c.ComposeService().Run(ctx, containerID, opts.Detach)
+	if err != nil {
+		return err
+	}
+	if opts.Detach {
+		fmt.Printf("%s", containerID)
+		return nil
+	}
+	if opts.Remove {
+		return c.ContainerService().Delete(ctx, containerID, containers.DeleteRequest{
+			Force: true,
+		})
+	}
+	return nil
+}

+ 2 - 1
cli/main.go

@@ -150,7 +150,8 @@ func main() {
 	opts.AddContextFlags(root.PersistentFlags())
 	opts.AddConfigFlags(root.PersistentFlags())
 	root.Flags().BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
-
+	root.PersistentFlags().SetInterspersed(false)
+	root.Flags().SetInterspersed(false)
 	walk(root, func(c *cobra.Command) {
 		c.Flags().BoolP("help", "h", false, "Help for "+c.Name())
 	})

+ 7 - 0
ecs/local/compose.go

@@ -27,6 +27,7 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/errdefs"
+	"github.com/pkg/errors"
 	"github.com/sanathkr/go-yaml"
 )
 
@@ -162,3 +163,9 @@ func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compo
 func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
 	return e.compose.List(ctx, projectName)
 }
+func (e ecsLocalSimulation) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	return "", errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose run")
+}
+func (e ecsLocalSimulation) Run(ctx context.Context, container string, detach bool) error {
+	return errdefs.ErrNotImplemented
+}

+ 33 - 0
ecs/run.go

@@ -0,0 +1,33 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package ecs
+
+import (
+	"context"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/errdefs"
+)
+
+func (b *ecsAPIService) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	return "", errdefs.ErrNotImplemented
+}
+
+func (b *ecsAPIService) Run(ctx context.Context, container string, detach bool) error {
+	return errdefs.ErrNotImplemented
+}

+ 7 - 0
example/backend.go

@@ -182,3 +182,10 @@ func (cs *composeService) Logs(ctx context.Context, projectName string, consumer
 func (cs *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }
+func (cs *composeService) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	return "", errdefs.ErrNotImplemented
+}
+
+func (cs *composeService) Run(ctx context.Context, container string, detach bool) error {
+	return errdefs.ErrNotImplemented
+}

+ 4 - 0
local/compose/convergence.go

@@ -61,6 +61,10 @@ func (s *composeService) ensureService(ctx context.Context, project *types.Proje
 		for i := 0; i < missing; i++ {
 			number := next + i
 			name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number)
+			if len(service.ContainerName) > 0 {
+				name = service.ContainerName
+			}
+
 			eg.Go(func() error {
 				return s.createContainer(ctx, project, service, name, number)
 			})

+ 250 - 0
local/compose/run.go

@@ -0,0 +1,250 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"sort"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/compose"
+	convert "github.com/docker/compose-cli/local/moby"
+	apitypes "github.com/docker/docker/api/types"
+	moby "github.com/docker/docker/pkg/stringid"
+)
+
+func (s *composeService) CreateOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (string, error) {
+	name := opts.Name
+	service, err := project.GetService(name)
+	if err != nil {
+		return "", err
+	}
+
+	err = s.ensureRequiredNetworks(ctx, project, service)
+	if err != nil {
+		return "", err
+	}
+	err = s.ensureRequiredVolumes(ctx, project, service)
+	if err != nil {
+		return "", err
+	}
+	// ensure required services are up and running before creating the oneoff container
+	err = s.ensureRequiredServices(ctx, project, service)
+	if err != nil {
+		return "", err
+	}
+
+	//apply options to service config
+	updateOneOffServiceConfig(&service, project.Name, opts)
+
+	err = s.createContainer(ctx, project, service, service.ContainerName, 1)
+	if err != nil {
+		return "", err
+	}
+
+	return service.ContainerName, err
+}
+
+func (s *composeService) Run(ctx context.Context, container string, detach bool) error {
+	if detach {
+		// start container
+		return s.apiClient.ContainerStart(ctx, container, apitypes.ContainerStartOptions{})
+	}
+
+	cnx, err := s.apiClient.ContainerAttach(ctx, container, apitypes.ContainerAttachOptions{
+		Stream: true,
+		Stdin:  true,
+		Stdout: true,
+		Stderr: true,
+		Logs:   true,
+	})
+	if err != nil {
+		return err
+	}
+	defer cnx.Close()
+
+	stdout := convert.ContainerStdout{HijackedResponse: cnx}
+	stdin := convert.ContainerStdin{HijackedResponse: cnx}
+
+	readChannel := make(chan error, 10)
+	writeChannel := make(chan error, 10)
+
+	go func() {
+		_, err := io.Copy(os.Stdout, cnx.Reader)
+		readChannel <- err
+	}()
+
+	go func() {
+		_, err := io.Copy(stdin, os.Stdin)
+		writeChannel <- err
+	}()
+
+	go func() {
+		<-ctx.Done()
+		stdout.Close() //nolint:errcheck
+		stdin.Close()  //nolint:errcheck
+	}()
+
+	// start container
+	err = s.apiClient.ContainerStart(ctx, container, apitypes.ContainerStartOptions{})
+	if err != nil {
+		return err
+	}
+
+	for {
+		select {
+		case err := <-readChannel:
+			return err
+		case err := <-writeChannel:
+			return err
+		}
+	}
+}
+
+func updateOneOffServiceConfig(service *types.ServiceConfig, projectName string, opts compose.RunOptions) {
+	if len(opts.Command) > 0 {
+		// custom command to run
+		service.Command = opts.Command
+	}
+	//service.Environment = opts.Environment
+	slug := moby.GenerateRandomID()
+	service.Scale = 1
+	service.ContainerName = fmt.Sprintf("%s_%s_run_%s", projectName, service.Name, moby.TruncateID(slug))
+	service.Labels = types.Labels{
+		"com.docker.compose.slug":   slug,
+		"com.docker.compose.oneoff": "True",
+	}
+	service.Tty = true
+	service.StdinOpen = true
+}
+
+func (s *composeService) ensureRequiredServices(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
+	requiredServices := getDependencyNames(project, service, func() []string {
+		return service.GetDependencies()
+	})
+	if len(requiredServices) > 0 {
+		// dependencies here
+		services, err := project.GetServices(requiredServices)
+		if err != nil {
+			return err
+		}
+		project.Services = services
+		err = s.ensureImagesExists(ctx, project)
+		if err != nil {
+			return err
+		}
+
+		err = InDependencyOrder(ctx, project, func(c context.Context, svc types.ServiceConfig) error {
+			return s.ensureService(c, project, svc)
+		})
+		if err != nil {
+			return err
+		}
+		return s.Start(ctx, project, nil)
+	}
+	return nil
+}
+
+func (s *composeService) ensureRequiredNetworks(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
+	networks := getDependentNetworkNames(project, service)
+	for k, network := range project.Networks {
+		if !contains(networks, network.Name) {
+			continue
+		}
+		if !network.External.External && network.Name != "" {
+			network.Name = fmt.Sprintf("%s_%s", project.Name, k)
+			project.Networks[k] = network
+		}
+		network.Labels = network.Labels.Add(networkLabel, k)
+		network.Labels = network.Labels.Add(projectLabel, project.Name)
+		network.Labels = network.Labels.Add(versionLabel, ComposeVersion)
+
+		err := s.ensureNetwork(ctx, network)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *composeService) ensureRequiredVolumes(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
+	volumes := getDependentVolumeNames(project, service)
+
+	for k, volume := range project.Volumes {
+		if !contains(volumes, volume.Name) {
+			continue
+		}
+		if !volume.External.External && volume.Name != "" {
+			volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
+			project.Volumes[k] = volume
+		}
+		volume.Labels = volume.Labels.Add(volumeLabel, k)
+		volume.Labels = volume.Labels.Add(projectLabel, project.Name)
+		volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion)
+		err := s.ensureVolume(ctx, volume)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type filterDependency func() []string
+
+func getDependencyNames(project *types.Project, service types.ServiceConfig, f filterDependency) []string {
+	names := f()
+	serviceNames := service.GetDependencies()
+	if len(serviceNames) == 0 {
+		return names
+	}
+	if len(serviceNames) > 0 {
+		services, _ := project.GetServices(serviceNames)
+		for _, s := range services {
+			svc := getDependencyNames(project, s, f)
+			names = append(names, svc...)
+		}
+	}
+	sort.Strings(names)
+	return unique(names)
+}
+
+func getDependentNetworkNames(project *types.Project, service types.ServiceConfig) []string {
+	return getDependencyNames(project, service, func() []string {
+		names := []string{}
+		for n := range service.Networks {
+			if contains(project.NetworkNames(), n) {
+				names = append(names, n)
+			}
+		}
+		return names
+	})
+}
+
+func getDependentVolumeNames(project *types.Project, service types.ServiceConfig) []string {
+	return getDependencyNames(project, service, func() []string {
+		names := []string{}
+		for _, v := range service.Volumes {
+			if contains(project.VolumeNames(), v.Source) {
+				names = append(names, v.Source)
+			}
+		}
+		return names
+	})
+}

+ 11 - 0
local/compose/util.go

@@ -38,3 +38,14 @@ func contains(slice []string, item string) bool {
 	}
 	return false
 }
+
+func unique(s []string) []string {
+	items := []string{}
+	for _, item := range s {
+		if contains(items, item) {
+			continue
+		}
+		items = append(items, item)
+	}
+	return items
+}