diff --git a/cmd/uncloud/service/root.go b/cmd/uncloud/service/root.go index 236d66eb..9b5c72d4 100644 --- a/cmd/uncloud/service/root.go +++ b/cmd/uncloud/service/root.go @@ -18,6 +18,8 @@ func NewRootCommand() *cobra.Command { NewRmCommand(), NewRunCommand(), NewScaleCommand(), + NewStopCommand(), + NewStartCommand(), ) return cmd } diff --git a/cmd/uncloud/service/start.go b/cmd/uncloud/service/start.go new file mode 100644 index 00000000..fc28d704 --- /dev/null +++ b/cmd/uncloud/service/start.go @@ -0,0 +1,50 @@ +//nolint:dupl +package service + +import ( + "context" + "fmt" + + "github.com/docker/compose/v2/pkg/progress" + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" +) + +type startOptions struct { + services []string +} + +func NewStartCommand() *cobra.Command { + opts := startOptions{} + cmd := &cobra.Command{ + Use: "start SERVICE [SERVICE...]", + Short: "Start one or more services.", + Long: "Start one or more services.", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + uncli := cmd.Context().Value("cli").(*cli.CLI) + opts.services = args + return start(cmd.Context(), uncli, opts) + }, + } + return cmd +} + +func start(ctx context.Context, uncli *cli.CLI, opts startOptions) error { + client, err := uncli.ConnectCluster(ctx) + if err != nil { + return fmt.Errorf("connect to cluster: %w", err) + } + defer client.Close() + + for _, s := range opts.services { + err = progress.RunWithTitle(ctx, func(ctx context.Context) error { + if err = client.StartService(ctx, s); err != nil { + return fmt.Errorf("start service '%s': %w", s, err) + } + return nil + }, uncli.ProgressOut(), "Starting service "+s) + } + + return err +} diff --git a/cmd/uncloud/service/stop.go b/cmd/uncloud/service/stop.go new file mode 100644 index 00000000..f5a61ad5 --- /dev/null +++ b/cmd/uncloud/service/stop.go @@ -0,0 +1,64 @@ +//nolint:dupl +package service + +import ( + "context" + "fmt" + + "github.com/docker/compose/v2/pkg/progress" + "github.com/docker/docker/api/types/container" + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" +) + +type stopOptions struct { + services []string + signal string + timeoutChanged bool + timeout int +} + +func NewStopCommand() *cobra.Command { + opts := stopOptions{} + cmd := &cobra.Command{ + Use: "stop SERVICE [SERVICE...]", + Short: "Stop one or more services.", + Long: "Stop one or more services.", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + uncli := cmd.Context().Value("cli").(*cli.CLI) + opts.services = args + opts.timeoutChanged = cmd.Flags().Changed("timeout") + return stop(cmd.Context(), uncli, opts) + }, + } + cmd.Flags().StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") + cmd.Flags().IntVarP(&opts.timeout, "timeout", "t", 0, "Seconds to wait before killing the container") + return cmd +} + +func stop(ctx context.Context, uncli *cli.CLI, opts stopOptions) error { + client, err := uncli.ConnectCluster(ctx) + if err != nil { + return fmt.Errorf("connect to cluster: %w", err) + } + defer client.Close() + + stopOpts := container.StopOptions{ + Signal: opts.signal, + } + if opts.timeoutChanged { + stopOpts.Timeout = &opts.timeout + } + + for _, s := range opts.services { + err = progress.RunWithTitle(ctx, func(ctx context.Context) error { + if err = client.StopService(ctx, s, stopOpts); err != nil { + return fmt.Errorf("stop service '%s': %w", s, err) + } + return nil + }, uncli.ProgressOut(), "Stopping service "+s) + } + + return err +} diff --git a/pkg/api/client.go b/pkg/api/client.go index f2208904..8bd920dd 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -48,6 +48,8 @@ type ServiceClient interface { RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error) InspectService(ctx context.Context, id string) (Service, error) RemoveService(ctx context.Context, id string) error + StopService(ctx context.Context, id string, opts container.StopOptions) error + StartService(ctx context.Context, id string) error } type VolumeClient interface { diff --git a/pkg/client/service.go b/pkg/client/service.go index 014eda39..08f0d42e 100644 --- a/pkg/client/service.go +++ b/pkg/client/service.go @@ -273,6 +273,80 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error { return err } +// StopService stops all containers on all machines that belong to the specified service. +// The id parameter can be either a service ID or name. +func (cli *Client) StopService(ctx context.Context, id string, opts container.StopOptions) error { + svc, err := cli.InspectService(ctx, id) + if err != nil { + return err + } + + wg := sync.WaitGroup{} + errCh := make(chan error) + + // Stop all containers on all machines that belong to the service. + for _, mc := range svc.Containers { + wg.Add(1) + + go func() { + defer wg.Done() + + err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, opts) + if err != nil { + errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err) + } + }() + } + + go func() { + wg.Wait() + close(errCh) + }() + + err = nil + for e := range errCh { + err = errors.Join(err, e) + } + return err +} + +// StartService starts all containers on all machines that belong to the specified service. +// The id parameter can be either a service ID or name. +func (cli *Client) StartService(ctx context.Context, id string) error { + svc, err := cli.InspectService(ctx, id) + if err != nil { + return err + } + + wg := sync.WaitGroup{} + errCh := make(chan error) + + // Start all containers on all machines that belong to the service. + for _, mc := range svc.Containers { + wg.Add(1) + + go func() { + defer wg.Done() + + err := cli.StartContainer(ctx, svc.ID, mc.Container.ID) + if err != nil { + errCh <- fmt.Errorf("start container '%s': %w", mc.Container.ID, err) + } + }() + } + + go func() { + wg.Wait() + close(errCh) + }() + + err = nil + for e := range errCh { + err = errors.Join(err, e) + } + return err +} + // ListServices returns a list of all services and their containers. func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) { machines, err := cli.ListMachines(ctx, nil)