mirror of https://github.com/docker/compose.git
Merge pull request #1003 from aiordache/compose_run_cmd
Add local `compose run` command
This commit is contained in:
commit
b2b9ce0a53
|
@ -201,3 +201,7 @@ 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) {
|
func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
|
||||||
return nil, errdefs.ErrNotImplemented
|
return nil, errdefs.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
return errdefs.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
|
@ -71,3 +71,7 @@ func (c *composeService) List(context.Context, string) ([]compose.Stack, error)
|
||||||
func (c *composeService) Convert(context.Context, *types.Project, compose.ConvertOptions) ([]byte, error) {
|
func (c *composeService) Convert(context.Context, *types.Project, compose.ConvertOptions) ([]byte, error) {
|
||||||
return nil, errdefs.ErrNotImplemented
|
return nil, errdefs.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
return errdefs.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
)
|
)
|
||||||
|
@ -46,6 +47,8 @@ type Service interface {
|
||||||
List(ctx context.Context, projectName string) ([]Stack, error)
|
List(ctx context.Context, projectName string) ([]Stack, error)
|
||||||
// Convert translate compose model into backend's native format
|
// Convert translate compose model into backend's native format
|
||||||
Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
|
Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
|
||||||
|
// RunOneOffContainer creates a service oneoff container and starts its dependencies
|
||||||
|
RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpOptions group options of the Up API
|
// UpOptions group options of the Up API
|
||||||
|
@ -66,6 +69,16 @@ type ConvertOptions struct {
|
||||||
Format string
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunOptions options to execute compose run
|
||||||
|
type RunOptions struct {
|
||||||
|
Name string
|
||||||
|
Command []string
|
||||||
|
Detach bool
|
||||||
|
AutoRemove bool
|
||||||
|
Writer io.Writer
|
||||||
|
Reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
// PortPublisher hold status about published port
|
// PortPublisher hold status about published port
|
||||||
type PortPublisher struct {
|
type PortPublisher struct {
|
||||||
URL string
|
URL string
|
||||||
|
|
|
@ -90,6 +90,7 @@ func Command(contextType string) *cobra.Command {
|
||||||
listCommand(),
|
listCommand(),
|
||||||
logsCommand(),
|
logsCommand(),
|
||||||
convertCommand(),
|
convertCommand(),
|
||||||
|
runCommand(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if contextType == store.LocalContextType || contextType == store.DefaultContextType {
|
if contextType == store.LocalContextType || contextType == store.DefaultContextType {
|
||||||
|
@ -99,7 +100,7 @@ func Command(contextType string) *cobra.Command {
|
||||||
pullCommand(),
|
pullCommand(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
command.Flags().SetInterspersed(false)
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/compose-spec/compose-go/types"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/docker/compose-cli/api/client"
|
||||||
|
"github.com/docker/compose-cli/api/compose"
|
||||||
|
"github.com/docker/compose-cli/progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runOptions struct {
|
||||||
|
Name string
|
||||||
|
Command []string
|
||||||
|
WorkingDir string
|
||||||
|
ConfigPaths []string
|
||||||
|
Environment []string
|
||||||
|
Detach 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 {
|
||||||
|
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.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
|
||||||
|
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")
|
||||||
|
|
||||||
|
runCmd.Flags().SetInterspersed(false)
|
||||||
|
return runCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRun(ctx context.Context, opts runOptions) error {
|
||||||
|
projectOpts := composeOptions{
|
||||||
|
ConfigPaths: opts.ConfigPaths,
|
||||||
|
WorkingDir: opts.WorkingDir,
|
||||||
|
Environment: opts.Environment,
|
||||||
|
}
|
||||||
|
c, project, err := setup(ctx, projectOpts, []string{opts.Name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
originalServices := project.Services
|
||||||
|
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
|
||||||
|
return "", startDependencies(ctx, c, project, opts.Name)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Services = originalServices
|
||||||
|
// start container and attach to container streams
|
||||||
|
runOpts := compose.RunOptions{
|
||||||
|
Name: opts.Name,
|
||||||
|
Command: opts.Command,
|
||||||
|
Detach: opts.Detach,
|
||||||
|
AutoRemove: opts.Remove,
|
||||||
|
Writer: os.Stdout,
|
||||||
|
Reader: os.Stdin,
|
||||||
|
}
|
||||||
|
return c.ComposeService().RunOneOffContainer(ctx, project, runOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDependencies(ctx context.Context, c *client.Client, project *types.Project, requestedService string) error {
|
||||||
|
originalServices := project.Services
|
||||||
|
dependencies := types.Services{}
|
||||||
|
for _, service := range originalServices {
|
||||||
|
if service.Name != requestedService {
|
||||||
|
dependencies = append(dependencies, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.Services = dependencies
|
||||||
|
if err := c.ComposeService().Create(ctx, project); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.ComposeService().Start(ctx, project, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/errdefs"
|
"github.com/docker/compose-cli/errdefs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/sanathkr/go-yaml"
|
"github.com/sanathkr/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -162,3 +163,6 @@ func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compo
|
||||||
func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
|
func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
|
||||||
return e.compose.List(ctx, projectName)
|
return e.compose.List(ctx, projectName)
|
||||||
}
|
}
|
||||||
|
func (e ecsLocalSimulation) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
return errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose run")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
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) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
return errdefs.ErrNotImplemented
|
||||||
|
}
|
|
@ -182,3 +182,6 @@ 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) {
|
func (cs *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
|
||||||
return nil, errdefs.ErrNotImplemented
|
return nil, errdefs.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
func (cs *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
return errdefs.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@ func (s *composeService) getContainerStreams(ctx context.Context, container moby
|
||||||
Stdin: true,
|
Stdin: true,
|
||||||
Stdout: true,
|
Stdout: true,
|
||||||
Stderr: true,
|
Stderr: true,
|
||||||
|
Logs: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -62,7 +62,7 @@ func (s *composeService) ensureService(ctx context.Context, project *types.Proje
|
||||||
number := next + i
|
number := next + i
|
||||||
name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number)
|
name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number)
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return s.createContainer(ctx, project, service, name, number)
|
return s.createContainer(ctx, project, service, name, number, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,10 +163,10 @@ func getScale(config types.ServiceConfig) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int) error {
|
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, autoRemove bool) error {
|
||||||
w := progress.ContextWriter(ctx)
|
w := progress.ContextWriter(ctx)
|
||||||
w.Event(progress.CreatingEvent(name))
|
w.Event(progress.CreatingEvent(name))
|
||||||
err := s.runContainer(ctx, project, service, name, number, nil)
|
err := s.createMobyContainer(ctx, project, service, name, number, nil, autoRemove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -191,7 +191,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.runContainer(ctx, project, service, name, number, &container)
|
err = s.createMobyContainer(ctx, project, service, name, number, &container, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -228,8 +228,8 @@ func (s *composeService) restartContainer(ctx context.Context, container moby.Co
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container) error {
|
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container, autoRemove bool) error {
|
||||||
containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, number, container)
|
containerConfig, hostConfig, networkingConfig, err := getCreateOptions(project, service, number, container, autoRemove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,20 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectNetworks(ctx, project); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectVolumes(ctx, project); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
||||||
|
return s.ensureService(c, project, service)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *composeService) ensureProjectNetworks(ctx context.Context, project *types.Project) error {
|
||||||
for k, network := range project.Networks {
|
for k, network := range project.Networks {
|
||||||
if !network.External.External && network.Name != "" {
|
if !network.External.External && network.Name != "" {
|
||||||
network.Name = fmt.Sprintf("%s_%s", project.Name, k)
|
network.Name = fmt.Sprintf("%s_%s", project.Name, k)
|
||||||
|
@ -57,7 +71,10 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) error {
|
||||||
for k, volume := range project.Volumes {
|
for k, volume := range project.Volumes {
|
||||||
if !volume.External.External && volume.Name != "" {
|
if !volume.External.External && volume.Name != "" {
|
||||||
volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
|
volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
|
||||||
|
@ -71,13 +88,10 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
|
||||||
return s.ensureService(c, project, service)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
|
func getCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container, autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
|
||||||
hash, err := jsonHash(s)
|
hash, err := jsonHash(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
@ -88,11 +102,12 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i
|
||||||
labels[k] = v
|
labels[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: change oneoffLabel value for containers started with `docker compose run`
|
|
||||||
labels[projectLabel] = p.Name
|
labels[projectLabel] = p.Name
|
||||||
labels[serviceLabel] = s.Name
|
labels[serviceLabel] = s.Name
|
||||||
labels[versionLabel] = ComposeVersion
|
labels[versionLabel] = ComposeVersion
|
||||||
labels[oneoffLabel] = "False"
|
if _, ok := s.Labels[oneoffLabel]; !ok {
|
||||||
|
labels[oneoffLabel] = "False"
|
||||||
|
}
|
||||||
labels[configHashLabel] = hash
|
labels[configHashLabel] = hash
|
||||||
labels[workingDirLabel] = p.WorkingDir
|
labels[workingDirLabel] = p.WorkingDir
|
||||||
labels[configFilesLabel] = strings.Join(p.ComposeFiles, ",")
|
labels[configFilesLabel] = strings.Join(p.ComposeFiles, ",")
|
||||||
|
@ -152,6 +167,7 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i
|
||||||
|
|
||||||
networkMode := getNetworkMode(p, s)
|
networkMode := getNetworkMode(p, s)
|
||||||
hostConfig := container.HostConfig{
|
hostConfig := container.HostConfig{
|
||||||
|
AutoRemove: autoRemove,
|
||||||
Mounts: mountOptions,
|
Mounts: mountOptions,
|
||||||
CapAdd: strslice.StrSlice(s.CapAdd),
|
CapAdd: strslice.StrSlice(s.CapAdd),
|
||||||
CapDrop: strslice.StrSlice(s.CapDrop),
|
CapDrop: strslice.StrSlice(s.CapDrop),
|
||||||
|
|
|
@ -91,16 +91,17 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
|
||||||
|
|
||||||
func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
|
func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
|
toDelete := container
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
eventName := "Container " + getContainerName(container)
|
eventName := "Container " + getContainerName(toDelete)
|
||||||
w.Event(progress.StoppingEvent(eventName))
|
w.Event(progress.StoppingEvent(eventName))
|
||||||
err := s.apiClient.ContainerStop(ctx, container.ID, nil)
|
err := s.apiClient.ContainerStop(ctx, toDelete.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
|
w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Event(progress.RemovingEvent(eventName))
|
w.Event(progress.RemovingEvent(eventName))
|
||||||
err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
|
err = s.apiClient.ContainerRemove(ctx, toDelete.ID, moby.ContainerRemoveOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
|
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
const (
|
const (
|
||||||
containerNumberLabel = "com.docker.compose.container-number"
|
containerNumberLabel = "com.docker.compose.container-number"
|
||||||
oneoffLabel = "com.docker.compose.oneoff"
|
oneoffLabel = "com.docker.compose.oneoff"
|
||||||
|
slugLabel = "com.docker.compose.slug"
|
||||||
projectLabel = "com.docker.compose.project"
|
projectLabel = "com.docker.compose.project"
|
||||||
volumeLabel = "com.docker.compose.volume"
|
volumeLabel = "com.docker.compose.volume"
|
||||||
workingDirLabel = "com.docker.compose.project.working_dir"
|
workingDirLabel = "com.docker.compose.project.working_dir"
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
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/types"
|
||||||
|
"github.com/docker/compose-cli/api/compose"
|
||||||
|
apitypes "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
moby "github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
|
||||||
|
originalServices := project.Services
|
||||||
|
var requestedService types.ServiceConfig
|
||||||
|
for _, service := range originalServices {
|
||||||
|
if service.Name == opts.Name {
|
||||||
|
requestedService = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Services = originalServices
|
||||||
|
if len(opts.Command) > 0 {
|
||||||
|
requestedService.Command = opts.Command
|
||||||
|
}
|
||||||
|
requestedService.Scale = 1
|
||||||
|
requestedService.Tty = true
|
||||||
|
requestedService.StdinOpen = true
|
||||||
|
|
||||||
|
slug := moby.GenerateRandomID()
|
||||||
|
requestedService.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, requestedService.Name, moby.TruncateID(slug))
|
||||||
|
requestedService.Labels = requestedService.Labels.Add(slugLabel, slug)
|
||||||
|
requestedService.Labels = requestedService.Labels.Add(oneoffLabel, "True")
|
||||||
|
|
||||||
|
if err := s.ensureImagesExists(ctx, project); err != nil { // all dependencies already checked, but might miss requestedService img
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.waitDependencies(ctx, project, requestedService); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.createContainer(ctx, project, requestedService, requestedService.ContainerName, 1, opts.AutoRemove); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
containerID := requestedService.ContainerName
|
||||||
|
|
||||||
|
if opts.Detach {
|
||||||
|
err := s.apiClient.ContainerStart(ctx, containerID, apitypes.ContainerStartOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(opts.Writer, containerID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", fmt.Sprintf("%s=%s", slugLabel, slug)),
|
||||||
|
),
|
||||||
|
All: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oneoffContainer := containers[0]
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
eg.Go(func() error {
|
||||||
|
return s.attachContainerStreams(ctx, oneoffContainer, true, opts.Reader, opts.Writer)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err = s.apiClient.ContainerStart(ctx, containerID, apitypes.ContainerStartOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
|
@ -103,6 +103,61 @@ func TestLocalComposeUp(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalComposeRun(t *testing.T) {
|
||||||
|
c := NewParallelE2eCLI(t, binDir)
|
||||||
|
|
||||||
|
t.Run("compose run", func(t *testing.T) {
|
||||||
|
res := c.RunDockerCmd("compose", "run", "-f", "./fixtures/run-test/docker-compose.yml", "back")
|
||||||
|
lines := Lines(res.Stdout())
|
||||||
|
assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check run container exited", func(t *testing.T) {
|
||||||
|
res := c.RunDockerCmd("ps", "--all")
|
||||||
|
lines := Lines(res.Stdout())
|
||||||
|
var runContainerID string
|
||||||
|
var truncatedSlug string
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
containerID := fields[len(fields)-1]
|
||||||
|
assert.Assert(t, !strings.HasPrefix(containerID, "run-test_front"))
|
||||||
|
if strings.HasPrefix(containerID, "run-test_back") {
|
||||||
|
//only the one-off container for back service
|
||||||
|
assert.Assert(t, strings.HasPrefix(containerID, "run-test_back_run_"), containerID)
|
||||||
|
truncatedSlug = strings.Replace(containerID, "run-test_back_run_", "", 1)
|
||||||
|
runContainerID = containerID
|
||||||
|
assert.Assert(t, strings.Contains(line, "Exited"), line)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(containerID, "run-test_db_1") {
|
||||||
|
assert.Assert(t, strings.Contains(line, "Up"), line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Assert(t, runContainerID != "")
|
||||||
|
res = c.RunDockerCmd("inspect", runContainerID)
|
||||||
|
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.container-number": "1"`})
|
||||||
|
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "run-test"`})
|
||||||
|
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "True",`})
|
||||||
|
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.slug": "` + truncatedSlug})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("compose run --rm", func(t *testing.T) {
|
||||||
|
res := c.RunDockerCmd("compose", "run", "-f", "./fixtures/run-test/docker-compose.yml", "--rm", "back", "/bin/sh", "-c", "echo Hello again")
|
||||||
|
lines := Lines(res.Stdout())
|
||||||
|
assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check run container removed", func(t *testing.T) {
|
||||||
|
res := c.RunDockerCmd("ps", "--all")
|
||||||
|
assert.Assert(t, strings.Contains(res.Stdout(), "run-test_back"), res.Stdout())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("down", func(t *testing.T) {
|
||||||
|
c.RunDockerCmd("compose", "down", "-f", "./fixtures/run-test/docker-compose.yml")
|
||||||
|
res := c.RunDockerCmd("ps", "--all")
|
||||||
|
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalComposeBuild(t *testing.T) {
|
func TestLocalComposeBuild(t *testing.T) {
|
||||||
c := NewParallelE2eCLI(t, binDir)
|
c := NewParallelE2eCLI(t, binDir)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
back:
|
||||||
|
image: alpine
|
||||||
|
command: echo "Hello there!!"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- backnet
|
||||||
|
db:
|
||||||
|
image: nginx
|
||||||
|
networks:
|
||||||
|
- backnet
|
||||||
|
volumes:
|
||||||
|
- data:/test
|
||||||
|
front:
|
||||||
|
image: nginx
|
||||||
|
networks:
|
||||||
|
- frontnet
|
||||||
|
networks:
|
||||||
|
frontnet:
|
||||||
|
backnet:
|
||||||
|
volumes:
|
||||||
|
data:
|
Loading…
Reference in New Issue