feat: Add uc service start/stop commands (#195)

* feat: Add `uc service start/stop` commands

* Add nolint:dupl for service start/stop commands

Co-authored-by: Pasha Sviderski <me@psviderski.name>

* Add --signal and --timeout options to uc service stop

* Check whether service stop --timeout flag was used to set options properly

---------

Co-authored-by: Pasha Sviderski <me@psviderski.name>
This commit is contained in:
Justin Bradford
2025-12-09 15:58:09 +10:00
committed by GitHub
co-authored by Pasha Sviderski
parent 4b4b721d49
commit a9a720f3c3
5 changed files with 192 additions and 0 deletions
+2
View File
@@ -18,6 +18,8 @@ func NewRootCommand() *cobra.Command {
NewRmCommand(), NewRmCommand(),
NewRunCommand(), NewRunCommand(),
NewScaleCommand(), NewScaleCommand(),
NewStopCommand(),
NewStartCommand(),
) )
return cmd return cmd
} }
+50
View File
@@ -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
}
+64
View File
@@ -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
}
+2
View File
@@ -48,6 +48,8 @@ type ServiceClient interface {
RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error) RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error)
InspectService(ctx context.Context, id string) (Service, error) InspectService(ctx context.Context, id string) (Service, error)
RemoveService(ctx context.Context, id string) 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 { type VolumeClient interface {
+74
View File
@@ -273,6 +273,80 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
return err 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. // ListServices returns a list of all services and their containers.
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) { func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)