Skip to content

Commit a9a720f

Browse files
jabrpsviderski
andauthored
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 <[email protected]> * 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 <[email protected]>
1 parent 4b4b721 commit a9a720f

File tree

5 files changed

+192
-0
lines changed

5 files changed

+192
-0
lines changed

cmd/uncloud/service/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func NewRootCommand() *cobra.Command {
1818
NewRmCommand(),
1919
NewRunCommand(),
2020
NewScaleCommand(),
21+
NewStopCommand(),
22+
NewStartCommand(),
2123
)
2224
return cmd
2325
}

cmd/uncloud/service/start.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//nolint:dupl
2+
package service
3+
4+
import (
5+
"context"
6+
"fmt"
7+
8+
"github.com/docker/compose/v2/pkg/progress"
9+
"github.com/psviderski/uncloud/internal/cli"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type startOptions struct {
14+
services []string
15+
}
16+
17+
func NewStartCommand() *cobra.Command {
18+
opts := startOptions{}
19+
cmd := &cobra.Command{
20+
Use: "start SERVICE [SERVICE...]",
21+
Short: "Start one or more services.",
22+
Long: "Start one or more services.",
23+
Args: cobra.MinimumNArgs(1),
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
uncli := cmd.Context().Value("cli").(*cli.CLI)
26+
opts.services = args
27+
return start(cmd.Context(), uncli, opts)
28+
},
29+
}
30+
return cmd
31+
}
32+
33+
func start(ctx context.Context, uncli *cli.CLI, opts startOptions) error {
34+
client, err := uncli.ConnectCluster(ctx)
35+
if err != nil {
36+
return fmt.Errorf("connect to cluster: %w", err)
37+
}
38+
defer client.Close()
39+
40+
for _, s := range opts.services {
41+
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
42+
if err = client.StartService(ctx, s); err != nil {
43+
return fmt.Errorf("start service '%s': %w", s, err)
44+
}
45+
return nil
46+
}, uncli.ProgressOut(), "Starting service "+s)
47+
}
48+
49+
return err
50+
}

cmd/uncloud/service/stop.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//nolint:dupl
2+
package service
3+
4+
import (
5+
"context"
6+
"fmt"
7+
8+
"github.com/docker/compose/v2/pkg/progress"
9+
"github.com/docker/docker/api/types/container"
10+
"github.com/psviderski/uncloud/internal/cli"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
type stopOptions struct {
15+
services []string
16+
signal string
17+
timeoutChanged bool
18+
timeout int
19+
}
20+
21+
func NewStopCommand() *cobra.Command {
22+
opts := stopOptions{}
23+
cmd := &cobra.Command{
24+
Use: "stop SERVICE [SERVICE...]",
25+
Short: "Stop one or more services.",
26+
Long: "Stop one or more services.",
27+
Args: cobra.MinimumNArgs(1),
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
uncli := cmd.Context().Value("cli").(*cli.CLI)
30+
opts.services = args
31+
opts.timeoutChanged = cmd.Flags().Changed("timeout")
32+
return stop(cmd.Context(), uncli, opts)
33+
},
34+
}
35+
cmd.Flags().StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
36+
cmd.Flags().IntVarP(&opts.timeout, "timeout", "t", 0, "Seconds to wait before killing the container")
37+
return cmd
38+
}
39+
40+
func stop(ctx context.Context, uncli *cli.CLI, opts stopOptions) error {
41+
client, err := uncli.ConnectCluster(ctx)
42+
if err != nil {
43+
return fmt.Errorf("connect to cluster: %w", err)
44+
}
45+
defer client.Close()
46+
47+
stopOpts := container.StopOptions{
48+
Signal: opts.signal,
49+
}
50+
if opts.timeoutChanged {
51+
stopOpts.Timeout = &opts.timeout
52+
}
53+
54+
for _, s := range opts.services {
55+
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
56+
if err = client.StopService(ctx, s, stopOpts); err != nil {
57+
return fmt.Errorf("stop service '%s': %w", s, err)
58+
}
59+
return nil
60+
}, uncli.ProgressOut(), "Stopping service "+s)
61+
}
62+
63+
return err
64+
}

pkg/api/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type ServiceClient interface {
4848
RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error)
4949
InspectService(ctx context.Context, id string) (Service, error)
5050
RemoveService(ctx context.Context, id string) error
51+
StopService(ctx context.Context, id string, opts container.StopOptions) error
52+
StartService(ctx context.Context, id string) error
5153
}
5254

5355
type VolumeClient interface {

pkg/client/service.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,80 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
273273
return err
274274
}
275275

276+
// StopService stops all containers on all machines that belong to the specified service.
277+
// The id parameter can be either a service ID or name.
278+
func (cli *Client) StopService(ctx context.Context, id string, opts container.StopOptions) error {
279+
svc, err := cli.InspectService(ctx, id)
280+
if err != nil {
281+
return err
282+
}
283+
284+
wg := sync.WaitGroup{}
285+
errCh := make(chan error)
286+
287+
// Stop all containers on all machines that belong to the service.
288+
for _, mc := range svc.Containers {
289+
wg.Add(1)
290+
291+
go func() {
292+
defer wg.Done()
293+
294+
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, opts)
295+
if err != nil {
296+
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
297+
}
298+
}()
299+
}
300+
301+
go func() {
302+
wg.Wait()
303+
close(errCh)
304+
}()
305+
306+
err = nil
307+
for e := range errCh {
308+
err = errors.Join(err, e)
309+
}
310+
return err
311+
}
312+
313+
// StartService starts all containers on all machines that belong to the specified service.
314+
// The id parameter can be either a service ID or name.
315+
func (cli *Client) StartService(ctx context.Context, id string) error {
316+
svc, err := cli.InspectService(ctx, id)
317+
if err != nil {
318+
return err
319+
}
320+
321+
wg := sync.WaitGroup{}
322+
errCh := make(chan error)
323+
324+
// Start all containers on all machines that belong to the service.
325+
for _, mc := range svc.Containers {
326+
wg.Add(1)
327+
328+
go func() {
329+
defer wg.Done()
330+
331+
err := cli.StartContainer(ctx, svc.ID, mc.Container.ID)
332+
if err != nil {
333+
errCh <- fmt.Errorf("start container '%s': %w", mc.Container.ID, err)
334+
}
335+
}()
336+
}
337+
338+
go func() {
339+
wg.Wait()
340+
close(errCh)
341+
}()
342+
343+
err = nil
344+
for e := range errCh {
345+
err = errors.Join(err, e)
346+
}
347+
return err
348+
}
349+
276350
// ListServices returns a list of all services and their containers.
277351
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
278352
machines, err := cli.ListMachines(ctx, nil)

0 commit comments

Comments
 (0)