Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions cmd/uncloud/context/ls.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
package context

import (
"encoding/json"
"fmt"
"maps"
"slices"

"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/cli/output"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra"
)

type listOptions struct {
output string
}

func NewListCommand() *cobra.Command {
opts := listOptions{}

cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List available cluster contexts.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(uncli)
return list(uncli, opts)
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return output.FlagValue(opts.output)
},
}

output.Flag(cmd, &opts.output)

return cmd
}

func list(uncli *cli.CLI) error {
func list(uncli *cli.CLI, opts listOptions) error {
if uncli.Config == nil {
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
}

if opts.output == "json" {
type Contexts struct { // Wrap in Contexts type to create array of Contexts.
Contexts map[string]*config.Context `json:"Contexts"`
}
data, _ := json.MarshalIndent(Contexts{uncli.Config.Contexts}, "", " ")
fmt.Println(string(data))
return nil
}

if len(uncli.Config.Contexts) == 0 {
fmt.Println("No contexts found")
return nil
Expand Down
24 changes: 24 additions & 0 deletions cmd/uncloud/image/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package image

import (
"context"
"encoding/json"
"fmt"
"os"
"slices"
Expand All @@ -17,6 +18,7 @@ import (

"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/output"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
Expand All @@ -25,6 +27,7 @@ import (
type listOptions struct {
machines []string
nameFilter string
output string
}

func NewListCommand() *cobra.Command {
Expand Down Expand Up @@ -57,11 +60,15 @@ func NewListCommand() *cobra.Command {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return output.FlagValue(opts.output)
},
}

cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Filter images by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
"(default is include all machines)")
output.Flag(cmd, &opts.output)

completion.MachinesFlag(cmd)

Expand Down Expand Up @@ -114,6 +121,12 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
// Collect all images from all machines.
var rows []imageRow

// Wrapper struct for json output.
type Images struct {
Images []image.Summary
}
images := Images{Images: []image.Summary{}}

for _, machineImages := range clusterImages {
// Get machine name for better readability.
machineName := machineImages.Metadata.Machine
Expand All @@ -126,6 +139,11 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
store = "containerd"
}

if opts.output == "json" {
images.Images = append(images.Images, machineImages.Images...)
continue
}

// Process each image for this machine.
for _, img := range machineImages.Images {
// Show the first 12 chars without 'sha256:' as the image ID like Docker does.
Expand Down Expand Up @@ -171,6 +189,12 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
}
}

if opts.output == "json" {
data, _ := json.MarshalIndent(images, "", " ")
fmt.Println(string(data))
return nil
}

if len(rows) == 0 {
if opts.nameFilter != "" {
fmt.Printf("No images matching '%s' found.\n", opts.nameFilter)
Expand Down
30 changes: 28 additions & 2 deletions cmd/uncloud/machine/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,46 @@ package machine

import (
"context"
"encoding/json"
"fmt"
"net/netip"
"strings"

"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/output"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)

type listOptions struct {
output string
}

func NewListCommand() *cobra.Command {
opts := listOptions{}

cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List machines in a cluster.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli)
return list(cmd.Context(), uncli, opts)
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return output.FlagValue(opts.output)
},
}

output.Flag(cmd, &opts.output)

return cmd
}

func list(ctx context.Context, uncli *cli.CLI) error {
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
client, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
Expand All @@ -37,6 +53,16 @@ func list(ctx context.Context, uncli *cli.CLI) error {
return fmt.Errorf("list machines: %w", err)
}

if opts.output == "json" {
// Wrap in Machines type to create array of Machines.
type Machines struct {
Machines api.MachineMembersList `json:"Machines"`
}
data, _ := json.MarshalIndent(Machines{machines}, "", " ")
fmt.Println(string(data))
return nil
}

// Print the list of machines in a table format.
t := tui.NewTable()
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", "MACHINE ID")
Expand Down
17 changes: 17 additions & 0 deletions cmd/uncloud/volume/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package volume

import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"

"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/output"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
Expand All @@ -16,6 +18,7 @@ import (
type listOptions struct {
machines []string
quiet bool
output string
}

func NewListCommand() *cobra.Command {
Expand All @@ -25,17 +28,22 @@ func NewListCommand() *cobra.Command {
Use: "ls",
Aliases: []string{"list"},
Short: "List volumes across all machines in the cluster.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return output.FlagValue(opts.output)
},
}

cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Filter volumes by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
"(default is include all machines)")
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
"Only display volume names.")
output.Flag(cmd, &opts.output)

completion.MachinesFlag(cmd)

Expand Down Expand Up @@ -63,6 +71,15 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
return fmt.Errorf("list volumes: %w", err)
}

if opts.output == "json" {
type Volumes struct { // Wrap in Volumes type to create array of Volumes.
Volumes []api.MachineVolume `json:"Volumes"`
}
data, _ := json.MarshalIndent(Volumes{volumes}, "", " ")
fmt.Println(string(data))
return nil
}

if len(volumes) == 0 {
if !opts.quiet {
fmt.Println("No volumes found.")
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
)

type Config struct {
CurrentContext string `yaml:"current_context"`
Contexts map[string]*Context `yaml:"contexts"`
CurrentContext string `yaml:"current_context" json:"CurrentContext"`
Contexts map[string]*Context `yaml:"contexts" json:"Contexts"`

// path is the file path config is read from.
path string
Expand Down
18 changes: 9 additions & 9 deletions internal/cli/config/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ import (

type MachineConnection struct {
// SSH uses the system ssh CLI command to connect. This is the default SSH connection method.
SSH SSHDestination `yaml:"ssh,omitempty"`
SSH SSHDestination `yaml:"ssh,omitempty" json:"SSH,omitempty"`
// SSHCLI is a backward-compatible alias for SSH.
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty" json:"SSHCli,omitempty"`
// SSHGo uses Go's built-in SSH library to connect.
SSHGo SSHDestination `yaml:"ssh_go,omitempty"`
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
SSHGo SSHDestination `yaml:"ssh_go,omitempty" json:"SSHGo,omitempty"`
SSHKeyFile string `yaml:"ssh_key_file,omitempty" json:"SSHKey,omitempty"`
// TCP is the address and port of the machine's API server.
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
TCP *netip.AddrPort `yaml:"tcp,omitempty" json:"TCP,omitempty"`
// Unix is the path to the machine's API unix socket.
Unix string `yaml:"unix,omitempty"`
Host string `yaml:"host,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty"`
MachineID string `yaml:"machine_id,omitempty"`
Unix string `yaml:"unix,omitempty" json:"Unix,omitempty"`
Host string `yaml:"host,omitempty" json:"Host,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty" json:"PublicKey,omitempty"`
MachineID string `yaml:"machine_id,omitempty" json:"MachineID,omitempty"`
}

func (c *MachineConnection) String() string {
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/config/context.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package config

type Context struct {
Name string `yaml:"-"`
Connections []MachineConnection `yaml:"connections"`
Name string `yaml:"-" json:"-"`
Connections []MachineConnection `yaml:"connections" json:"Connections"`
}

func (c *Context) SetDefaultConnection(index int) {
Expand Down
22 changes: 22 additions & 0 deletions internal/cli/output/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package output

import (
"fmt"

"github.com/spf13/cobra"
)

func Flag(cmd *cobra.Command, value *string) {
cmd.Flags().StringVarP(value, "output", "o", "",
"Output format. Currently only 'json' is supported.")
}

func FlagValue(value string) error {
switch value {
case "json":
return nil
case "":
return nil
}
return fmt.Errorf("invalid --output format, want 'json'")
}
16 changes: 16 additions & 0 deletions internal/machine/api/pb/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ func (ip *IP) Equal(other *IP) bool {
return bytes.Equal(ip.Ip, other.Ip)
}

func (ip *IP) MarshalJSON() ([]byte, error) {
if ip == nil {
return nil, nil
}
addr, _ := ip.ToAddr()
return []byte("\"" + addr.String() + "\""), nil
}

func NewIPPort(ap netip.AddrPort) *IPPort {
return &IPPort{Ip: NewIP(ap.Addr()), Port: uint32(ap.Port())}
}
Expand All @@ -39,6 +47,14 @@ func (ipp *IPPort) ToAddrPort() (netip.AddrPort, error) {
return netip.AddrPortFrom(addr, uint16(ipp.Port)), nil
}

func (ipp *IPPort) MarshalJSON() ([]byte, error) {
if ipp == nil {
return nil, nil
}
addrPort, _ := ipp.ToAddrPort()
return []byte("\"" + addrPort.String() + "\""), nil
}

func NewIPPrefix(p netip.Prefix) *IPPrefix {
return &IPPrefix{Ip: NewIP(p.Addr()), Bits: uint32(p.Bits())}
}
Expand Down
3 changes: 2 additions & 1 deletion website/docs/9-cli-reference/uc_ctx_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ uc ctx ls [flags]
## Options

```
-h, --help help for ls
-h, --help help for ls
-o, --output string Output format. Currently only 'json' is supported.
```

## Options inherited from parent commands
Expand Down
1 change: 1 addition & 0 deletions website/docs/9-cli-reference/uc_image_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ uc image ls [REPO:[TAG]] [flags]
```
-h, --help help for ls
-m, --machine strings Filter images by machine name or ID. Can be specified multiple times or as a comma-separated list. (default is include all machines)
-o, --output string Output format. Currently only 'json' is supported.
```

## Options inherited from parent commands
Expand Down
1 change: 1 addition & 0 deletions website/docs/9-cli-reference/uc_images.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ uc images [IMAGE] [flags]
```
-h, --help help for images
-m, --machine strings Filter images by machine name or ID. Can be specified multiple times or as a comma-separated list. (default is include all machines)
-o, --output string Output format. Currently only 'json' is supported.
```

## Options inherited from parent commands
Expand Down
Loading
Loading