mirror of https://github.com/docker/compose.git
Add `compose run` command
Signed-off-by: aiordache <anca.iordache@docker.com>
This commit is contained in:
parent
a17e397df3
commit
412385c495
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue