mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat(cli): add JSON output format for 'uc machine ls', remove MACHINE ID column from table output
This commit is contained in:
@@ -2,6 +2,7 @@ package machine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,19 +15,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
|
var output string
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List machines in a cluster.",
|
Short: "List machines in a cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return list(cmd.Context(), uncli)
|
return list(cmd.Context(), uncli, output)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&output, "output", "o", "",
|
||||||
|
"Output format: 'json' or empty for a human-readable table.")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI) error {
|
func list(ctx context.Context, uncli *cli.CLI, output string) error {
|
||||||
|
if output != "" && output != "json" {
|
||||||
|
return fmt.Errorf("unsupported output format '%s' (supported: json)", output)
|
||||||
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectCluster(ctx)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
@@ -38,10 +49,19 @@ func list(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if output == "json" {
|
||||||
|
data, err := json.MarshalIndent(machines.ToNative(), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal machines: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Print the list of machines in a table format.
|
// Print the list of machines in a table format.
|
||||||
t := tui.NewTable()
|
t := tui.NewTable()
|
||||||
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS",
|
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS",
|
||||||
"OS", "KERNEL", "ARCH", "DOCKER", "VERSION", "MACHINE ID")
|
"OS", "KERNEL", "ARCH", "DOCKER", "VERSION")
|
||||||
|
|
||||||
for _, member := range machines {
|
for _, member := range machines {
|
||||||
m := member.Machine
|
m := member.Machine
|
||||||
@@ -96,7 +116,6 @@ func list(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
arch,
|
arch,
|
||||||
dockerVersion,
|
dockerVersion,
|
||||||
daemonVersion,
|
daemonVersion,
|
||||||
member.Machine.Id,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+94
-1
@@ -1,6 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import "github.com/psviderski/uncloud/internal/machine/api/pb"
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
// MachineFilter defines criteria to filter machines in ListMachines.
|
// MachineFilter defines criteria to filter machines in ListMachines.
|
||||||
type MachineFilter struct {
|
type MachineFilter struct {
|
||||||
@@ -21,3 +26,91 @@ func (m MachineMembersList) FindByNameOrID(nameOrID string) *pb.MachineMember {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToNative returns the Go native representation of the protobuf machine members.
|
||||||
|
func (m MachineMembersList) ToNative() []MachineMember {
|
||||||
|
infos := make([]MachineMember, len(m))
|
||||||
|
for i, member := range m {
|
||||||
|
infos[i] = machineMemberFromProto(member)
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineMember is the JSON-serializable view of a machine member.
|
||||||
|
type MachineMember struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
State string
|
||||||
|
Network MachineNetwork
|
||||||
|
PublicIP netip.Addr
|
||||||
|
|
||||||
|
DaemonVersion string
|
||||||
|
DockerVersion string
|
||||||
|
Hostname string
|
||||||
|
Arch string
|
||||||
|
OSPrettyName string
|
||||||
|
KernelVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineNetwork describes a machine's WireGuard network configuration.
|
||||||
|
type MachineNetwork struct {
|
||||||
|
Subnet netip.Prefix
|
||||||
|
ManagementIP netip.Addr
|
||||||
|
Endpoints []netip.AddrPort
|
||||||
|
// PublicKey is the WireGuard public key.
|
||||||
|
PublicKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func machineMemberFromProto(pbMember *pb.MachineMember) MachineMember {
|
||||||
|
m := pbMember.Machine
|
||||||
|
member := MachineMember{
|
||||||
|
ID: m.Id,
|
||||||
|
Name: m.Name,
|
||||||
|
State: capitalise(pbMember.State.String()),
|
||||||
|
DaemonVersion: m.DaemonVersion,
|
||||||
|
DockerVersion: m.DockerVersion,
|
||||||
|
Hostname: m.Hostname,
|
||||||
|
Arch: m.Arch,
|
||||||
|
OSPrettyName: m.OsPrettyName,
|
||||||
|
KernelVersion: m.KernelVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.PublicIp != nil {
|
||||||
|
if ip, err := m.PublicIp.ToAddr(); err == nil {
|
||||||
|
member.PublicIP = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Network != nil {
|
||||||
|
network := MachineNetwork{
|
||||||
|
PublicKey: m.Network.PublicKey,
|
||||||
|
}
|
||||||
|
if m.Network.Subnet != nil {
|
||||||
|
if subnet, err := m.Network.Subnet.ToPrefix(); err == nil {
|
||||||
|
network.Subnet = subnet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Network.ManagementIp != nil {
|
||||||
|
if ip, err := m.Network.ManagementIp.ToAddr(); err == nil {
|
||||||
|
network.ManagementIP = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints := make([]netip.AddrPort, 0, len(m.Network.Endpoints))
|
||||||
|
for _, ep := range m.Network.Endpoints {
|
||||||
|
if addrPort, err := ep.ToAddrPort(); err == nil {
|
||||||
|
endpoints = append(endpoints, addrPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
network.Endpoints = endpoints
|
||||||
|
member.Network = network
|
||||||
|
}
|
||||||
|
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalise(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMachineMembersList_Info(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
publicKey := []byte{0x01, 0x02, 0x03, 0x04}
|
||||||
|
members := MachineMembersList{
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Id: "abc123",
|
||||||
|
Name: "vm-1",
|
||||||
|
Hostname: "vm-1.example.com",
|
||||||
|
Arch: "amd64",
|
||||||
|
OsPrettyName: "Ubuntu 24.04.4 LTS",
|
||||||
|
KernelVersion: "6.8.0-31-generic",
|
||||||
|
DockerVersion: "27.1.1",
|
||||||
|
DaemonVersion: "0.9.0",
|
||||||
|
PublicIp: pb.NewIP(netip.MustParseAddr("203.0.113.5")),
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
Subnet: pb.NewIPPrefix(netip.MustParsePrefix("10.210.0.0/24")),
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.210.0.1")),
|
||||||
|
Endpoints: []*pb.IPPort{
|
||||||
|
pb.NewIPPort(netip.MustParseAddrPort("203.0.113.5:51820")),
|
||||||
|
},
|
||||||
|
PublicKey: publicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
State: pb.MachineMember_UP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
infos := members.ToNative()
|
||||||
|
require.Len(t, infos, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, MachineMember{
|
||||||
|
ID: "abc123",
|
||||||
|
Name: "vm-1",
|
||||||
|
State: "Up",
|
||||||
|
Hostname: "vm-1.example.com",
|
||||||
|
Arch: "amd64",
|
||||||
|
OSPrettyName: "Ubuntu 24.04.4 LTS",
|
||||||
|
KernelVersion: "6.8.0-31-generic",
|
||||||
|
DockerVersion: "27.1.1",
|
||||||
|
DaemonVersion: "0.9.0",
|
||||||
|
PublicIP: netip.MustParseAddr("203.0.113.5"),
|
||||||
|
Network: MachineNetwork{
|
||||||
|
Subnet: netip.MustParsePrefix("10.210.0.0/24"),
|
||||||
|
ManagementIP: netip.MustParseAddr("10.210.0.1"),
|
||||||
|
Endpoints: []netip.AddrPort{netip.MustParseAddrPort("203.0.113.5:51820")},
|
||||||
|
PublicKey: publicKey,
|
||||||
|
},
|
||||||
|
}, infos[0])
|
||||||
|
|
||||||
|
// The JSON output must use PascalCase keys to stay consistent with `docker inspect` and the
|
||||||
|
// rest of Uncloud's JSON output.
|
||||||
|
data, err := json.Marshal(infos)
|
||||||
|
require.NoError(t, err)
|
||||||
|
out := string(data)
|
||||||
|
for _, key := range []string{
|
||||||
|
`"ID"`, `"Name"`, `"State"`, `"Hostname"`, `"Arch"`, `"OSPrettyName"`, `"KernelVersion"`,
|
||||||
|
`"DockerVersion"`, `"DaemonVersion"`, `"PublicIP"`, `"Network"`,
|
||||||
|
`"Subnet"`, `"ManagementIP"`, `"Endpoints"`, `"PublicKey"`,
|
||||||
|
} {
|
||||||
|
assert.Contains(t, out, key)
|
||||||
|
}
|
||||||
|
assert.Contains(t, out, `"PublicIP":"203.0.113.5"`)
|
||||||
|
assert.Contains(t, out, `"Subnet":"10.210.0.0/24"`)
|
||||||
|
assert.Contains(t, out, `"Endpoints":["203.0.113.5:51820"]`)
|
||||||
|
assert.Contains(t, out, `"PublicKey":"AQIDBA=="`)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user