diff --git a/cmd/compose/alpha.go b/cmd/compose/alpha.go index 5eaeaa9e2af..88b4d23d077 100644 --- a/cmd/compose/alpha.go +++ b/cmd/compose/alpha.go @@ -32,6 +32,7 @@ func alphaCommand(p *ProjectOptions, backend api.Service) *cobra.Command { cmd.AddCommand( watchCommand(p, backend), vizCommand(p, backend), + scaleCommand(p, backend), ) return cmd } diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go new file mode 100644 index 00000000000..60916d89e94 --- /dev/null +++ b/cmd/compose/scale.go @@ -0,0 +1,103 @@ +/* + 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/pkg/errors" + "golang.org/x/exp/maps" + "strconv" + "strings" + + "github.com/docker/compose/v2/pkg/api" + "github.com/spf13/cobra" +) + +type scaleOptions struct { + *ProjectOptions + noDeps bool +} + +func scaleCommand(p *ProjectOptions, 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, backend, opts, serviceTuples) + }), + ValidArgsFunction: completeServiceNames(p), + } + flags := scaleCmd.Flags() + flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.") + + return scaleCmd +} + +func runScale(ctx context.Context, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error { + services := maps.Keys(serviceReplicaTuples) + project, err := opts.ToProject(services) + if err != nil { + return err + } + + if opts.noDeps { + project.ForServices(services, types.IgnoreDependencies) + } + +tuplesLoop: + for key, value := range serviceReplicaTuples { + for i, service := range project.Services { + if service.Name == key { + if service.Deploy == nil { + service.Deploy = &types.DeployConfig{} + } + scale := uint64(value) + service.Deploy.Replicas = &scale + project.Services[i] = service + continue tuplesLoop + } + } + } + + 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 +} diff --git a/docs/reference/compose_alpha_scale.md b/docs/reference/compose_alpha_scale.md new file mode 100644 index 00000000000..3cda50e206b --- /dev/null +++ b/docs/reference/compose_alpha_scale.md @@ -0,0 +1,15 @@ +# docker compose alpha scale + + +Scale services + +### Options + +| Name | Type | Default | Description | +|:------------|:-----|:--------|:--------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--no-deps` | | | Don't start linked services. | + + + + diff --git a/docs/reference/docker_compose_alpha.yaml b/docs/reference/docker_compose_alpha.yaml index 2598c0e8818..0a71bfd96ad 100644 --- a/docs/reference/docker_compose_alpha.yaml +++ b/docs/reference/docker_compose_alpha.yaml @@ -4,9 +4,11 @@ long: Experimental commands pname: docker compose plink: docker_compose.yaml cname: + - docker compose alpha scale - docker compose alpha viz - docker compose alpha watch clink: + - docker_compose_alpha_scale.yaml - docker_compose_alpha_viz.yaml - docker_compose_alpha_watch.yaml inherited_options: diff --git a/docs/reference/docker_compose_alpha_scale.yaml b/docs/reference/docker_compose_alpha_scale.yaml new file mode 100644 index 00000000000..cc381493fa3 --- /dev/null +++ b/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 + diff --git a/go.mod b/go.mod index f38fc2551ac..f1ca9941550 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,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.7.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.9.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect diff --git a/pkg/api/api.go b/pkg/api/api.go index 28595941b7c..1e6fdce49ed 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -86,6 +86,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 { diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index 4547cb26617..132ad31ac3c 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -55,6 +55,7 @@ type ServiceProxy struct { DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error) VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error) WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error) + ScaleFn func(ctx context.Context, project *types.Project, options ScaleOptions) error interceptors []Interceptor } @@ -97,6 +98,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 } @@ -343,6 +345,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) } diff --git a/pkg/compose/scale.go b/pkg/compose/scale.go new file mode 100644 index 00000000000..513e270fa12 --- /dev/null +++ b/pkg/compose/scale.go @@ -0,0 +1,35 @@ +/* +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()) +}