1
0
Эх сурвалжийг харах

add scale command

Signed-off-by: Guillaume Lours <[email protected]>
Guillaume Lours 2 жил өмнө
parent
commit
1a98a70b8a

+ 1 - 0
cmd/compose/compose.go

@@ -476,6 +476,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
 		createCommand(&opts, dockerCli, backend),
 		copyCommand(&opts, dockerCli, backend),
 		waitCommand(&opts, dockerCli, backend),
+		scaleCommand(&opts, dockerCli, backend),
 		alphaCommand(&opts, dockerCli, backend),
 	)
 

+ 108 - 0
cmd/compose/scale.go

@@ -0,0 +1,108 @@
+/*
+   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"
+	"strconv"
+	"strings"
+
+	"github.com/docker/cli/cli/command"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/pkg/errors"
+	"golang.org/x/exp/maps"
+
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/spf13/cobra"
+)
+
+type scaleOptions struct {
+	*ProjectOptions
+	noDeps bool
+}
+
+func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
+	opts := scaleOptions{
+		ProjectOptions: p,
+	}
+	scaleCmd := &cobra.Command{
+		Use:   "scale [SERVICE=REPLICAS...]",
+		Short: "Scale services ",
+		Args:  cobra.MinimumNArgs(1),
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			serviceTuples, err := parseServicesReplicasArgs(args)
+			if err != nil {
+				return err
+			}
+			return runScale(ctx, dockerCli, backend, opts, serviceTuples)
+		}),
+		ValidArgsFunction: completeServiceNames(dockerCli, p),
+	}
+	flags := scaleCmd.Flags()
+	flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
+
+	return scaleCmd
+}
+
+func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
+	services := maps.Keys(serviceReplicaTuples)
+	project, err := opts.ToProject(dockerCli, services)
+	if err != nil {
+		return err
+	}
+
+	if opts.noDeps {
+		if err := project.ForServices(services, types.IgnoreDependencies); err != nil {
+			return err
+		}
+	}
+
+	for key, value := range serviceReplicaTuples {
+		for i, service := range project.Services {
+			if service.Name != key {
+				continue
+			}
+			if service.Deploy == nil {
+				service.Deploy = &types.DeployConfig{}
+			}
+			scale := uint64(value)
+			service.Deploy.Replicas = &scale
+			project.Services[i] = service
+			break
+		}
+	}
+
+	return backend.Scale(ctx, project, api.ScaleOptions{Services: services})
+}
+
+func parseServicesReplicasArgs(args []string) (map[string]int, error) {
+	serviceReplicaTuples := map[string]int{}
+	for _, arg := range args {
+		key, val, ok := strings.Cut(arg, "=")
+		if !ok || key == "" || val == "" {
+			return nil, errors.Errorf("Invalide scale specifier %q.", arg)
+		}
+		intValue, err := strconv.Atoi(val)
+
+		if err != nil {
+			return nil, errors.Errorf("Invalide scale specifier, can't parse replicate value to int %q.", arg)
+		}
+		serviceReplicaTuples[key] = intValue
+	}
+	return serviceReplicaTuples, nil
+}

+ 1 - 0
docs/reference/compose.md

@@ -26,6 +26,7 @@ Define and run multi-container applications with Docker.
 | [`restart`](compose_restart.md) | Restart service containers                                              |
 | [`rm`](compose_rm.md)           | Removes stopped service containers                                      |
 | [`run`](compose_run.md)         | Run a one-off command on a service.                                     |
+| [`scale`](compose_scale.md)     | Scale services                                                          |
 | [`start`](compose_start.md)     | Start services                                                          |
 | [`stop`](compose_stop.md)       | Stop services                                                           |
 | [`top`](compose_top.md)         | Display the running processes                                           |

+ 15 - 0
docs/reference/compose_alpha_scale.md

@@ -0,0 +1,15 @@
+# docker compose alpha scale
+
+<!---MARKER_GEN_START-->
+Scale services 
+
+### Options
+
+| Name        | Type | Default | Description                     |
+|:------------|:-----|:--------|:--------------------------------|
+| `--dry-run` |      |         | Execute command in dry run mode |
+| `--no-deps` |      |         | Don't start linked services.    |
+
+
+<!---MARKER_GEN_END-->
+

+ 15 - 0
docs/reference/compose_scale.md

@@ -0,0 +1,15 @@
+# docker compose scale
+
+<!---MARKER_GEN_START-->
+Scale services 
+
+### Options
+
+| Name        | Type | Default | Description                     |
+|:------------|:-----|:--------|:--------------------------------|
+| `--dry-run` |      |         | Execute command in dry run mode |
+| `--no-deps` |      |         | Don't start linked services.    |
+
+
+<!---MARKER_GEN_END-->
+

+ 2 - 0
docs/reference/docker_compose.yaml

@@ -165,6 +165,7 @@ cname:
     - docker compose restart
     - docker compose rm
     - docker compose run
+    - docker compose scale
     - docker compose start
     - docker compose stop
     - docker compose top
@@ -192,6 +193,7 @@ clink:
     - docker_compose_restart.yaml
     - docker_compose_rm.yaml
     - docker_compose_run.yaml
+    - docker_compose_scale.yaml
     - docker_compose_start.yaml
     - docker_compose_stop.yaml
     - docker_compose_top.yaml

+ 35 - 0
docs/reference/docker_compose_alpha_scale.yaml

@@ -0,0 +1,35 @@
+command: docker compose alpha scale
+short: Scale services
+long: Scale services
+usage: docker compose alpha scale [SERVICE=REPLICAS...]
+pname: docker compose alpha
+plink: docker_compose_alpha.yaml
+options:
+    - option: no-deps
+      value_type: bool
+      default_value: "false"
+      description: Don't start linked services.
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+inherited_options:
+    - option: dry-run
+      value_type: bool
+      default_value: "false"
+      description: Execute command in dry run mode
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+deprecated: false
+hidden: false
+experimental: false
+experimentalcli: true
+kubernetes: false
+swarm: false
+

+ 35 - 0
docs/reference/docker_compose_scale.yaml

@@ -0,0 +1,35 @@
+command: docker compose scale
+short: Scale services
+long: Scale services
+usage: docker compose scale [SERVICE=REPLICAS...]
+pname: docker compose
+plink: docker_compose.yaml
+options:
+    - option: no-deps
+      value_type: bool
+      default_value: "false"
+      description: Don't start linked services.
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+inherited_options:
+    - option: dry-run
+      value_type: bool
+      default_value: "false"
+      description: Execute command in dry run mode
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+deprecated: false
+hidden: false
+experimental: false
+experimentalcli: false
+kubernetes: false
+swarm: false
+

+ 2 - 1
go.mod

@@ -51,6 +51,8 @@ require (
 	gotest.tools/v3 v3.5.0
 )
 
+require golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
+
 require (
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -155,7 +157,6 @@ require (
 	go.opentelemetry.io/otel/metric v0.37.0 // indirect
 	go.opentelemetry.io/proto/otlp v0.19.0 // indirect
 	golang.org/x/crypto v0.11.0 // indirect
-	golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
 	golang.org/x/mod v0.11.0 // indirect
 	golang.org/x/net v0.12.0 // indirect
 	golang.org/x/oauth2 v0.10.0 // indirect

+ 6 - 0
pkg/api/api.go

@@ -88,6 +88,12 @@ type Service interface {
 	Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
 	// Wait blocks until at least one of the services' container exits
 	Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
+	// Scale manages numbers of container instances running per service
+	Scale(ctx context.Context, project *types.Project, options ScaleOptions) error
+}
+
+type ScaleOptions struct {
+	Services []string
 }
 
 type WaitOptions struct {

+ 9 - 0
pkg/api/proxy.go

@@ -56,6 +56,7 @@ type ServiceProxy struct {
 	VizFn                func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
 	WaitFn               func(ctx context.Context, projectName string, options WaitOptions) (int64, error)
 	PublishFn            func(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
+	ScaleFn              func(ctx context.Context, project *types.Project, options ScaleOptions) error
 	interceptors         []Interceptor
 }
 
@@ -99,6 +100,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
 	s.DryRunModeFn = service.DryRunMode
 	s.VizFn = service.Viz
 	s.WaitFn = service.Wait
+	s.ScaleFn = service.Scale
 	return s
 }
 
@@ -349,6 +351,13 @@ func (s *ServiceProxy) Wait(ctx context.Context, projectName string, options Wai
 	return s.WaitFn(ctx, projectName, options)
 }
 
+func (s *ServiceProxy) Scale(ctx context.Context, project *types.Project, options ScaleOptions) error {
+	if s.ScaleFn == nil {
+		return ErrNotImplemented
+	}
+	return s.ScaleFn(ctx, project, options)
+}
+
 func (s *ServiceProxy) MaxConcurrency(i int) {
 	s.MaxConcurrencyFn(i)
 }

+ 36 - 0
pkg/compose/scale.go

@@ -0,0 +1,36 @@
+/*
+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/v2/internal/tracing"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/progress"
+)
+
+func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
+	return progress.Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(project), func(ctx context.Context) error {
+		err := s.create(ctx, project, api.CreateOptions{Services: options.Services})
+		if err != nil {
+			return err
+		}
+		return s.start(ctx, project.Name, api.StartOptions{Project: project, Services: options.Services}, nil)
+
+	}), s.stdinfo())
+}

+ 15 - 0
pkg/e2e/fixtures/scale/compose.yaml

@@ -0,0 +1,15 @@
+services:
+  back:
+    image: nginx:alpine
+    depends_on:
+      - db
+  db:
+    image: nginx:alpine
+  front:
+    image: nginx:alpine
+    deploy:
+      replicas: 2
+  dbadmin:
+    image: nginx:alpine
+    deploy:
+      replicas: 0

+ 110 - 0
pkg/e2e/scale_test.go

@@ -0,0 +1,110 @@
+/*
+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 e2e
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	testify "github.com/stretchr/testify/assert"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/icmd"
+)
+
+const NO_STATE_TO_CHECK = ""
+
+func TestScaleBasicCases(t *testing.T) {
+	c := NewCLI(t, WithEnv(
+		"COMPOSE_PROJECT_NAME=scale-basic-tests"))
+
+	reset := func() {
+		c.RunDockerComposeCmd(t, "down", "--rmi", "all")
+	}
+	t.Cleanup(reset)
+	res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d")
+	res.Assert(t, icmd.Success)
+
+	t.Log("scale up one service")
+	res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=2")
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-dbadmin", "Started", 2)
+
+	t.Log("scale up 2 services")
+	res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "front=3", "back=2")
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-front", "Running", 2)
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-front", "Started", 1)
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-back", "Running", 1)
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-back", "Started", 1)
+
+	t.Log("scale down one service")
+	res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=1")
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-dbadmin", "Running", 1)
+
+	t.Log("scale to 0 a service")
+	res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "dbadmin=0")
+	assert.Check(t, res.Stdout() == "", res.Stdout())
+
+	t.Log("scale down 2 services")
+	res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "front=2", "back=1")
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-front", "Running", 2)
+	assert.Check(t, !strings.Contains(res.Combined(), "Container scale-basic-tests-front-3  Running"), res.Combined())
+	checkServiceContainer(t, res.Combined(), "scale-basic-tests-back", "Running", 1)
+}
+
+func TestScaleWithDepsCases(t *testing.T) {
+	c := NewCLI(t, WithEnv(
+		"COMPOSE_PROJECT_NAME=scale-deps-tests"))
+
+	reset := func() {
+		c.RunDockerComposeCmd(t, "down", "--rmi", "all")
+	}
+	t.Cleanup(reset)
+	res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2")
+	res.Assert(t, icmd.Success)
+
+	res = c.RunDockerComposeCmd(t, "ps")
+	checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 2)
+
+	t.Log("scale up 1 service with --no-deps")
+	_ = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "--no-deps", "back=2")
+	res = c.RunDockerComposeCmd(t, "ps")
+	checkServiceContainer(t, res.Combined(), "scale-deps-tests-back", NO_STATE_TO_CHECK, 2)
+	checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 2)
+
+	t.Log("scale up 1 service without --no-deps")
+	_ = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "scale", "back=2")
+	res = c.RunDockerComposeCmd(t, "ps")
+	checkServiceContainer(t, res.Combined(), "scale-deps-tests-back", NO_STATE_TO_CHECK, 2)
+	checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 1)
+}
+
+func checkServiceContainer(t *testing.T, stdout, containerName, containerState string, count int) {
+	found := 0
+	lines := strings.Split(stdout, "\n")
+	for _, line := range lines {
+		if strings.Contains(line, containerName) && strings.Contains(line, containerState) {
+			found++
+		}
+	}
+	if found == count {
+		return
+	}
+	errMessage := fmt.Sprintf("expected %d but found %d instance(s) of container %s in stoud", count, found, containerName)
+	if containerState != "" {
+		errMessage += fmt.Sprintf(" with expected state %s", containerState)
+	}
+	testify.Fail(t, errMessage, stdout)
+}

+ 14 - 0
pkg/mocks/mock_docker_compose_api.go

@@ -351,6 +351,20 @@ func (mr *MockServiceMockRecorder) RunOneOffContainer(ctx, project, opts interfa
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOneOffContainer", reflect.TypeOf((*MockService)(nil).RunOneOffContainer), ctx, project, opts)
 }
 
+// Scale mocks base method.
+func (m *MockService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Scale", ctx, project, options)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Scale indicates an expected call of Scale.
+func (mr *MockServiceMockRecorder) Scale(ctx, project, options interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scale", reflect.TypeOf((*MockService)(nil).Scale), ctx, project, options)
+}
+
 // Start mocks base method.
 func (m *MockService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
 	m.ctrl.T.Helper()