Skip to content

Commit f75afa0

Browse files
committed
Merge branch 'pasha/machine-rm'
2 parents 2b4da1e + 0f38b12 commit f75afa0

File tree

9 files changed

+386
-63
lines changed

9 files changed

+386
-63
lines changed

cmd/uncloud/machine/ls.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import (
1414
)
1515

1616
func NewListCommand() *cobra.Command {
17-
var clusterContext string
17+
var contextName string
1818
cmd := &cobra.Command{
1919
Use: "ls",
2020
Aliases: []string{"list"},
2121
Short: "List machines in a cluster.",
2222
RunE: func(cmd *cobra.Command, args []string) error {
2323
uncli := cmd.Context().Value("cli").(*cli.CLI)
24-
return list(cmd.Context(), uncli, clusterContext)
24+
return list(cmd.Context(), uncli, contextName)
2525
},
2626
}
2727
cmd.Flags().StringVarP(
28-
&clusterContext, "context", "c", "",
28+
&contextName, "context", "c", "",
2929
"Name of the cluster context. (default is the current context)",
3030
)
3131
return cmd
@@ -68,7 +68,8 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
6868
}
6969

7070
if _, err = fmt.Fprintf(
71-
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
71+
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP,
72+
strings.Join(endpoints, ", "),
7273
); err != nil {
7374
return fmt.Errorf("write row: %w", err)
7475
}

cmd/uncloud/machine/rm.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package machine
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"maps"
8+
"slices"
9+
"strings"
10+
"sync"
11+
12+
"github.com/charmbracelet/lipgloss"
13+
"github.com/charmbracelet/lipgloss/tree"
14+
"github.com/docker/compose/v2/pkg/progress"
15+
"github.com/docker/docker/api/types/container"
16+
"github.com/psviderski/uncloud/internal/cli"
17+
"github.com/psviderski/uncloud/pkg/api"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
type removeOptions struct {
22+
force bool
23+
yes bool
24+
context string
25+
}
26+
27+
func NewRmCommand() *cobra.Command {
28+
opts := removeOptions{}
29+
30+
cmd := &cobra.Command{
31+
Use: "rm MACHINE",
32+
Aliases: []string{"remove", "delete"},
33+
Short: "Remove a machine from a cluster.",
34+
Args: cobra.ExactArgs(1),
35+
RunE: func(cmd *cobra.Command, args []string) error {
36+
uncli := cmd.Context().Value("cli").(*cli.CLI)
37+
return remove(cmd.Context(), uncli, args[0], opts)
38+
},
39+
}
40+
41+
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
42+
"Name of the cluster context. (default is the current context)")
43+
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
44+
"Do not prompt for confirmation before removing the machine.")
45+
46+
return cmd
47+
}
48+
49+
func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts removeOptions) error {
50+
client, err := uncli.ConnectCluster(ctx, opts.context)
51+
if err != nil {
52+
return fmt.Errorf("connect to cluster: %w", err)
53+
}
54+
defer client.Close()
55+
56+
// Verify the machine exists and list all service containers on it including stopped ones.
57+
listCtx, machines, err := api.ProxyMachinesContext(ctx, client, []string{machineName})
58+
if err != nil {
59+
return err
60+
}
61+
if len(machines) == 0 {
62+
return fmt.Errorf("machine '%s' not found in the cluster", machineName)
63+
}
64+
m := machines[0].Machine
65+
66+
listOpts := container.ListOptions{All: true}
67+
machineContainers, err := client.Docker.ListServiceContainers(listCtx, "", listOpts)
68+
if err != nil {
69+
return fmt.Errorf("list containers: %w", err)
70+
}
71+
containers := machineContainers[0].Containers
72+
73+
if len(containers) > 0 {
74+
plural := ""
75+
if len(containers) > 1 {
76+
plural = "s"
77+
}
78+
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
79+
fmt.Println(formatContainerTree(containers))
80+
fmt.Println()
81+
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
82+
"and remove it from the cluster.")
83+
} else {
84+
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
85+
fmt.Println("This will reset the machine to the uninitialised state and remove it from the cluster.")
86+
}
87+
88+
if !opts.yes {
89+
confirmed, err := cli.Confirm()
90+
if err != nil {
91+
return fmt.Errorf("confirm removal: %w", err)
92+
}
93+
if !confirmed {
94+
fmt.Println("Cancelled. Machine was not removed.")
95+
return nil
96+
}
97+
}
98+
99+
if len(containers) > 0 {
100+
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
101+
return removeContainers(ctx, client, containers)
102+
}, uncli.ProgressOut(), "Removing containers")
103+
104+
if err != nil {
105+
return fmt.Errorf("remove containers: %w", err)
106+
}
107+
fmt.Println()
108+
}
109+
110+
// TODO: 4. Implement and call Reset via Machine API to reset the machine state to uninitialised.
111+
// TODO: 5. Remove the machine from the cluster store.
112+
113+
return fmt.Errorf("resetting machine is not fully implemented yet")
114+
//fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
115+
//return nil
116+
}
117+
118+
// formatContainerTree formats a list of containers grouped by service as a tree structure.
119+
func formatContainerTree(containers []api.ServiceContainer) string {
120+
if len(containers) == 0 {
121+
return ""
122+
}
123+
124+
// Group containers by service.
125+
serviceContainers := make(map[string][]api.ServiceContainer)
126+
for _, ctr := range containers {
127+
serviceName := ctr.ServiceName()
128+
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
129+
}
130+
131+
// Build tree output.
132+
var output []string
133+
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
134+
for _, serviceName := range serviceNames {
135+
ctrs := serviceContainers[serviceName]
136+
mode := ctrs[0].ServiceMode()
137+
138+
// Format a tree for the service with its containers.
139+
plural := ""
140+
if len(ctrs) > 1 {
141+
plural = "s"
142+
}
143+
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
144+
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
145+
146+
// Add containers as children.
147+
for _, ctr := range ctrs {
148+
state, _ := ctr.HumanState()
149+
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
150+
t.Child(info)
151+
}
152+
153+
output = append(output, t.String())
154+
}
155+
156+
return strings.Join(output, "\n")
157+
}
158+
159+
// removeContainers removes the given service containers from the machine.
160+
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
161+
if len(containers) == 0 {
162+
return nil
163+
}
164+
165+
wg := sync.WaitGroup{}
166+
errCh := make(chan error)
167+
168+
for _, ctr := range containers {
169+
wg.Add(1)
170+
go func(c api.ServiceContainer) {
171+
defer wg.Done()
172+
173+
// Gracefully stop the container before removing it.
174+
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
175+
if err != nil && !errors.Is(err, api.ErrNotFound) {
176+
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
177+
}
178+
179+
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
180+
// Remove anonymous volumes created by the container.
181+
RemoveVolumes: true,
182+
})
183+
if err != nil && !errors.Is(err, api.ErrNotFound) {
184+
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
185+
}
186+
}(ctr)
187+
}
188+
189+
go func() {
190+
wg.Wait()
191+
close(errCh)
192+
}()
193+
194+
var err error
195+
for e := range errCh {
196+
err = errors.Join(err, e)
197+
}
198+
199+
return err
200+
}

cmd/uncloud/machine/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func NewRootCommand() *cobra.Command {
1414
NewAddCommand(),
1515
NewInitCommand(),
1616
NewListCommand(),
17+
NewRmCommand(),
1718
NewTokenCommand(),
1819
)
1920
return cmd

go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ module github.com/psviderski/uncloud
22

33
go 1.23.0
44

5-
toolchain go1.23.2
6-
75
require (
86
github.com/BurntSushi/toml v1.4.0
97
github.com/Masterminds/semver v1.5.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
105105
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
106106
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
107107
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
108+
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
109+
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
108110
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
109111
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
110112
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -153,6 +155,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
153155
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
154156
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
155157
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
158+
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
159+
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
156160
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
157161
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
158162
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=

0 commit comments

Comments
 (0)