Skip to content

Commit 59b96cc

Browse files
committed
feat: init 'machine rm' command that outputs service containers that will be removed
1 parent 4c77fe2 commit 59b96cc

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package machine
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
"strings"
9+
10+
"github.com/charmbracelet/lipgloss"
11+
"github.com/charmbracelet/lipgloss/tree"
12+
"github.com/docker/docker/api/types/container"
13+
"github.com/psviderski/uncloud/internal/cli"
14+
"github.com/psviderski/uncloud/pkg/api"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
type removeOptions struct {
19+
force bool
20+
yes bool
21+
context string
22+
}
23+
24+
func NewRmCommand() *cobra.Command {
25+
opts := removeOptions{}
26+
27+
cmd := &cobra.Command{
28+
Use: "rm MACHINE",
29+
Aliases: []string{"remove", "delete"},
30+
Short: "Remove a machine from a cluster.",
31+
Args: cobra.ExactArgs(1),
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
uncli := cmd.Context().Value("cli").(*cli.CLI)
34+
return remove(cmd.Context(), uncli, args[0], opts)
35+
},
36+
}
37+
38+
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
39+
"Name of the cluster context. (default is the current context)")
40+
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
41+
"Do not prompt for confirmation before removing the machine.")
42+
43+
return cmd
44+
}
45+
46+
func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts removeOptions) error {
47+
client, err := uncli.ConnectCluster(ctx, opts.context)
48+
if err != nil {
49+
return fmt.Errorf("connect to cluster: %w", err)
50+
}
51+
defer client.Close()
52+
53+
// Verify the machine exists and list all service containers on it including stopped ones.
54+
listCtx, machines, err := api.ProxyMachinesContext(ctx, client, []string{machineName})
55+
if err != nil {
56+
return err
57+
}
58+
if len(machines) == 0 {
59+
return fmt.Errorf("machine '%s' not found in the cluster", machineName)
60+
}
61+
m := machines[0].Machine
62+
63+
listOpts := container.ListOptions{All: true}
64+
machineContainers, err := client.Docker.ListServiceContainers(listCtx, "", listOpts)
65+
if err != nil {
66+
return fmt.Errorf("list containers: %w", err)
67+
}
68+
containers := machineContainers[0].Containers
69+
70+
if len(containers) > 0 {
71+
fmt.Printf("Found %d service containers on machine '%s':\n\n", len(containers), m.Name)
72+
fmt.Println(formatContainerTree(containers))
73+
fmt.Println()
74+
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
75+
"and remove it from the cluster.")
76+
} else {
77+
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
78+
fmt.Println("This will reset the machine to the uninitialised state and remove it from the cluster.")
79+
}
80+
81+
if !opts.yes {
82+
confirmed, err := cli.Confirm()
83+
if err != nil {
84+
return fmt.Errorf("confirm removal: %w", err)
85+
}
86+
if !confirmed {
87+
fmt.Println("Cancelled. Machine was not removed.")
88+
return nil
89+
}
90+
}
91+
92+
// TODO: 3. Remove all service containers on the machine.
93+
// TODO: 4. Implement and call ResetMachine via Machine API to reset the machine state to uninitialised.
94+
// TODO: 5. Remove the machine from the cluster store.
95+
96+
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
97+
return nil
98+
}
99+
100+
// formatContainerTree formats a list of containers grouped by service as a tree structure.
101+
func formatContainerTree(containers []api.ServiceContainer) string {
102+
if len(containers) == 0 {
103+
return ""
104+
}
105+
106+
// Group containers by service.
107+
serviceContainers := make(map[string][]api.ServiceContainer)
108+
for _, ctr := range containers {
109+
serviceName := ctr.ServiceName()
110+
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
111+
}
112+
113+
// Build tree output.
114+
var output []string
115+
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
116+
for _, serviceName := range serviceNames {
117+
ctrs := serviceContainers[serviceName]
118+
mode := ctrs[0].ServiceMode()
119+
120+
// Format a tree for the service with its containers.
121+
plural := ""
122+
if len(ctrs) > 1 {
123+
plural = "s"
124+
}
125+
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
126+
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
127+
128+
// Add containers as children.
129+
for _, ctr := range ctrs {
130+
state, _ := ctr.HumanState()
131+
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
132+
t.Child(info)
133+
}
134+
135+
output = append(output, t.String())
136+
}
137+
138+
return strings.Join(output, "\n")
139+
}

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

0 commit comments

Comments
 (0)