diff --git a/local/compose/create.go b/local/compose/create.go index 2eafbf2b7..26bea4c2b 100644 --- a/local/compose/create.go +++ b/local/compose/create.go @@ -44,6 +44,20 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err return err } + if err := s.ensureProjectNetworks(ctx, project); err != nil { + return err + } + + if err := s.ensureProjectNetworks(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 { if !network.External.External && network.Name != "" { 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 nil +} +func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) error { for k, volume := range project.Volumes { if !volume.External.External && volume.Name != "" { volume.Name = fmt.Sprintf("%s_%s", project.Name, k) @@ -71,10 +88,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err return err } } - - return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - return s.ensureService(c, project, service) - }) + return nil } func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { @@ -88,11 +102,15 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i labels[k] = v } - // TODO: change oneoffLabel value for containers started with `docker compose run` labels[projectLabel] = p.Name labels[serviceLabel] = s.Name labels[versionLabel] = ComposeVersion - labels[oneoffLabel] = "False" + if _, ok := s.Labels[oneoffLabel]; ok { + labels[oneoffLabel] = s.Labels[oneoffLabel] + labels[slugLabel] = s.Labels[slugLabel] + } else { + labels[oneoffLabel] = "False" + } labels[configHashLabel] = hash labels[workingDirLabel] = p.WorkingDir labels[configFilesLabel] = strings.Join(p.ComposeFiles, ",") diff --git a/local/compose/labels.go b/local/compose/labels.go index ce36bf49a..0928f26f0 100644 --- a/local/compose/labels.go +++ b/local/compose/labels.go @@ -25,6 +25,7 @@ import ( const ( containerNumberLabel = "com.docker.compose.container-number" oneoffLabel = "com.docker.compose.oneoff" + slugLabel = "com.docker.compose.slug" projectLabel = "com.docker.compose.project" volumeLabel = "com.docker.compose.volume" workingDirLabel = "com.docker.compose.project.working_dir" diff --git a/local/compose/run.go b/local/compose/run.go index 4d41949ca..08584ad91 100644 --- a/local/compose/run.go +++ b/local/compose/run.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "os" - "sort" "github.com/compose-spec/compose-go/types" "github.com/docker/compose-cli/api/compose" @@ -37,12 +36,11 @@ func (s *composeService) CreateOneOffContainer(ctx context.Context, project *typ return "", err } - err = s.ensureRequiredNetworks(ctx, project, service) - if err != nil { + if err := s.ensureProjectNetworks(ctx, project); err != nil { return "", err } - err = s.ensureRequiredVolumes(ctx, project, service) - if err != nil { + + if err := s.ensureProjectVolumes(ctx, project); err != nil { return "", err } // ensure required services are up and running before creating the oneoff container @@ -128,123 +126,27 @@ func updateOneOffServiceConfig(service *types.ServiceConfig, projectName string, 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", + slugLabel: slug, + oneoffLabel: "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 := s.ensureImagesExists(ctx, project) + if err != nil { + return err + } - err = InDependencyOrder(ctx, project, func(c context.Context, svc types.ServiceConfig) error { + err = InDependencyOrder(ctx, project, func(c context.Context, svc types.ServiceConfig) error { + if svc.Name != service.Name { // only start dependencies, not service to run one-off 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 + return nil }) + if err != nil { + return err + } + return s.Start(ctx, project, nil) } diff --git a/local/compose/util.go b/local/compose/util.go index 3a025ed62..dd9cbbcfe 100644 --- a/local/compose/util.go +++ b/local/compose/util.go @@ -38,14 +38,3 @@ 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 -} diff --git a/tests/compose-e2e/compose_test.go b/tests/compose-e2e/compose_test.go index ddf99b51e..c034f02eb 100644 --- a/tests/compose-e2e/compose_test.go +++ b/tests/compose-e2e/compose_test.go @@ -103,6 +103,57 @@ 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") + res.Assert(t, icmd.Expected{Out: "Hello there!!"}) + }) + + t.Run("check run container exited", func(t *testing.T) { + res := c.RunDockerCmd("ps", "--all") + lines := Lines(res.Stdout()) + var runContainerID 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) + 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": "`}) + }) + + t.Run("compose run --rm", func(t *testing.T) { + res := c.RunDockerCmd("compose", "run", "-f", "./fixtures/run-test/docker-compose.yml", "--rm", "back") + res.Assert(t, icmd.Expected{Out: "Hello there!!"}) + }) + + 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) { c := NewParallelE2eCLI(t, binDir) diff --git a/tests/compose-e2e/fixtures/run-test/docker-compose.yml b/tests/compose-e2e/fixtures/run-test/docker-compose.yml new file mode 100644 index 000000000..6387a0d72 --- /dev/null +++ b/tests/compose-e2e/fixtures/run-test/docker-compose.yml @@ -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: {} \ No newline at end of file