chore: rename cluster term to context in CLI and config, convert config toml -> yaml

This commit is contained in:
Pavel Sviderski
2025-04-01 21:29:32 +10:00
parent 0e80f2e5b2
commit f467fad84c
24 changed files with 197 additions and 163 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ reset-dev:
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud" ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
demo-reset: demo-reset:
rm -fv ~/.config/uncloud/config.toml rm -fv ~/.config/uncloud/config.yaml
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1" ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall" ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall" ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
+5 -4
View File
@@ -3,12 +3,13 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"os" "os"
"strings" "strings"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/cmd/ucind/cluster" "github.com/psviderski/uncloud/cmd/ucind/cluster"
"github.com/psviderski/uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
) )
func main() { func main() {
@@ -44,9 +45,9 @@ func main() {
} }
// TODO: allow to override using UNCLOUD_CONFIG env var. // TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml", cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"path to the Uncloud configuration file.") "path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
cmd.AddCommand( cmd.AddCommand(
cluster.NewRootCommand(), cluster.NewRootCommand(),
+8 -7
View File
@@ -4,6 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"slices"
"strings"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
@@ -12,15 +16,12 @@ import (
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"maps"
"slices"
"strings"
) )
type deployOptions struct { type deployOptions struct {
image string image string
machine string machine string
cluster string context string
} }
func NewDeployCommand() *cobra.Command { func NewDeployCommand() *cobra.Command {
@@ -42,15 +43,15 @@ func NewDeployCommand() *cobra.Command {
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "", cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Machine names to deploy to (comma-separated). (default is all machines)") "Machine names to deploy to (comma-separated). (default is all machines)")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster to deploy to. (default is the current cluster)", "Name of the cluster context to deploy to. (default is the current context)",
) )
return cmd return cmd
} }
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster) clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+6 -5
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
@@ -11,14 +13,13 @@ import (
"github.com/psviderski/uncloud/pkg/client/compose" "github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"strings"
) )
type deployOptions struct { type deployOptions struct {
files []string files []string
services []string services []string
cluster string context string
} }
// NewDeployCommand creates a new command to deploy services from a Compose file. // NewDeployCommand creates a new command to deploy services from a Compose file.
@@ -41,8 +42,8 @@ func NewDeployCommand() *cobra.Command {
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil, cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)") "One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "", cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster to deploy to (default is the current cluster)") "Name of the cluster context to deploy to (default is the current context)")
return cmd return cmd
} }
@@ -62,7 +63,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
} }
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster) clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+6 -5
View File
@@ -4,15 +4,16 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"github.com/psviderski/uncloud/internal/cli"
) )
type releaseOptions struct { type releaseOptions struct {
cluster string context string
} }
func NewReleaseCommand() *cobra.Command { func NewReleaseCommand() *cobra.Command {
@@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command {
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error { func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.cluster) client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
@@ -18,7 +19,7 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
type reserveOptions struct { type reserveOptions struct {
endpoint string endpoint string
cluster string context string
} }
func NewReserveCommand() *cobra.Command { func NewReserveCommand() *cobra.Command {
@@ -36,15 +37,15 @@ func NewReserveCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint, cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.") "API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error { func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster) clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+5 -4
View File
@@ -4,13 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type showOptions struct { type showOptions struct {
cluster string context string
} }
func NewShowCommand() *cobra.Command { func NewShowCommand() *cobra.Command {
@@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command {
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error { func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster) clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+7 -6
View File
@@ -4,6 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
@@ -16,8 +19,6 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
) )
type addOptions struct { type addOptions struct {
@@ -25,7 +26,7 @@ type addOptions struct {
noCaddy bool noCaddy bool
publicIP string publicIP string
sshKey string sshKey string
cluster string context string
} }
func NewAddCommand() *cobra.Command { func NewAddCommand() *cobra.Command {
@@ -66,8 +67,8 @@ func NewAddCommand() *cobra.Command {
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster to add the machine to. (default is the current cluster)", "Name of the cluster context to add the machine to. (default is the current context)",
) )
return cmd return cmd
} }
@@ -87,7 +88,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
publicIP = &ip publicIP = &ip
} }
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP) machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.context, opts.name, publicIP)
if err != nil { if err != nil {
return err return err
} }
+9 -7
View File
@@ -3,6 +3,8 @@ package machine
import ( import (
"context" "context"
"fmt" "fmt"
"net/netip"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns" "github.com/psviderski/uncloud/cmd/uncloud/dns"
@@ -11,7 +13,6 @@ import (
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net/netip"
) )
type initOptions struct { type initOptions struct {
@@ -22,15 +23,16 @@ type initOptions struct {
noDNS bool noDNS bool
publicIP string publicIP string
sshKey string sshKey string
cluster string context string
} }
func NewInitCommand() *cobra.Command { func NewInitCommand() *cobra.Command {
opts := initOptions{} opts := initOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "init [USER@HOST:PORT]", Use: "init [USER@HOST:PORT]",
Short: "Initialise a new cluster that consists of the local or remote machine.", Short: "Initialise a new cluster with a remote machine as the first member.",
// TODO: include usage examples of initialising a local and remote machine. // TODO: include usage examples of initialising a remote machine.
// TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
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)
@@ -80,8 +82,8 @@ func NewInitCommand() *cobra.Command {
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster in the local config if initialising a remote machine.", "Name of the cluster context in the local config.",
) )
return cmd return cmd
@@ -107,7 +109,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
publicIP = &ip publicIP = &ip
} }
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP) client, err := uncli.InitCluster(ctx, remoteMachine, opts.context, opts.name, netPrefix, publicIP)
if err != nil { if err != nil {
return err return err
} }
+6 -5
View File
@@ -3,29 +3,30 @@ package machine
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/spf13/cobra"
"net/netip" "net/netip"
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
var cluster string var clusterContext 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, cluster) return list(cmd.Context(), uncli, clusterContext)
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&cluster, "cluster", "c", "", &clusterContext, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
+3 -3
View File
@@ -53,7 +53,7 @@ func main() {
configPath := fs.ExpandHomeDir(opts.configPath) configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn) uncli, err := cli.New(configPath, conn)
if err != nil { if err != nil {
return fmt.Errorf("initialize CLI: %w", err) return fmt.Errorf("initialise CLI: %w", err)
} }
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli)) cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
return nil return nil
@@ -64,9 +64,9 @@ func main() {
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+ "Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port") "Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var. // TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.toml", cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"Path to the Uncloud configuration file.") "Path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
cmd.AddCommand( cmd.AddCommand(
NewDeployCommand(), NewDeployCommand(),
+6 -5
View File
@@ -3,14 +3,15 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"os" "os"
"text/tabwriter" "text/tabwriter"
"time" "time"
"github.com/spf13/cobra" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
) )
type inspectOptions struct { type inspectOptions struct {
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
+5 -4
View File
@@ -3,11 +3,12 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
@@ -22,8 +23,8 @@ func NewListCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&cluster, "cluster", "c", "", &cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
+4 -3
View File
@@ -3,11 +3,12 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
type rmOptions struct { type rmOptions struct {
@@ -29,8 +30,8 @@ func NewRmCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
+5 -4
View File
@@ -3,13 +3,14 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"slices"
"strings"
) )
type runOptions struct { type runOptions struct {
@@ -79,8 +80,8 @@ func NewRunCommand() *cobra.Command {
"/host/path:/container/path[:ro]. Can be specified multiple times.") "/host/path:/container/path[:ro]. Can be specified multiple times.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster to run the service in. (default is the current cluster)", "Name of the cluster context to run the service in. (default is the current context)",
) )
return cmd return cmd
+2 -2
View File
@@ -40,8 +40,8 @@ func NewScaleCommand() *cobra.Command {
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
+1 -1
View File
@@ -16,7 +16,7 @@ In addition, any ports for any workloads you want to expose will need to be open
### Configuration ### Configuration
Uncloud stores its configuration in `~/.config/uncloud/config.toml`. If you wish to reinitialise a cluster, simply remove it from this config. Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
### Initialisation ### Initialisation
+1 -1
View File
@@ -52,6 +52,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
google.golang.org/grpc v1.68.1 google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.36.3 google.golang.org/protobuf v1.36.3
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.36.3 modernc.org/sqlite v1.36.3
) )
@@ -310,7 +311,6 @@ require (
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
+67 -58
View File
@@ -4,6 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
@@ -13,14 +16,12 @@ import (
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector" "github.com/psviderski/uncloud/pkg/client/connector"
"net/netip"
"os"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
const defaultClusterName = "default" const defaultContextName = "default"
type CLI struct { type CLI struct {
config *config.Config config *config.Config
@@ -45,61 +46,68 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
}, nil }, nil
} }
func (cli *CLI) CreateCluster(name string) error { func (cli *CLI) CreateContext(name string) error {
if _, ok := cli.config.Clusters[name]; ok { if _, ok := cli.config.Contexts[name]; ok {
return fmt.Errorf("cluster %q already exists", name) return fmt.Errorf("context '%s' already exists", name)
} }
cli.config.Clusters[name] = &config.Cluster{ cli.config.Contexts[name] = &config.Context{
Name: name, Name: name,
} }
return cli.config.Save() return cli.config.Save()
} }
func (cli *CLI) SetCurrentCluster(name string) error { func (cli *CLI) SetCurrentContext(name string) error {
if _, ok := cli.config.Clusters[name]; !ok { if _, ok := cli.config.Contexts[name]; !ok {
return api.ErrNotFound return api.ErrNotFound
} }
cli.config.CurrentCluster = name cli.config.CurrentContext = name
return cli.config.Save() return cli.config.Save()
} }
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified. // ConnectCluster connects to a cluster using the given context name or the current context if not specified.
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead. // If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) { func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
if cli.conn != nil { if cli.conn != nil {
return connectCluster(ctx, *cli.conn) return connectCluster(ctx, *cli.conn)
} }
if len(cli.config.Clusters) == 0 { if len(cli.config.Contexts) == 0 {
return nil, errors.New( return nil, fmt.Errorf(
"no clusters found in the Uncloud config. " + "no cluster contexts found in the Uncloud config (%s). "+
"Please initialise a cluster with `uncloud machine init` first", "Please initialise a cluster with 'uncloud machine init' first",
cli.config.Path(),
) )
} }
if clusterName == "" { if contextName == "" {
// If the cluster is not specified, use the current cluster if set. // If the cluster is not specified, use the current cluster if set.
if cli.config.CurrentCluster == "" { if cli.config.CurrentContext == "" {
return nil, errors.New(
"the current cluster is not set in the Uncloud config. " +
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
)
}
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"current cluster %q not found in the config. "+ "the current cluster context is not set in the Uncloud config (%s). "+
"Please specify a cluster with the --cluster flag or update current_cluster in the config", "Please specify the context with the '--context' flag or set 'current_context' in the config",
cli.config.CurrentCluster, cli.config.Path(),
) )
} }
clusterName = cli.config.CurrentCluster if _, ok := cli.config.Contexts[cli.config.CurrentContext]; !ok {
return nil, fmt.Errorf(
"current cluster context '%s' not found in the Uncloud config (%s). "+
"Please specify the context with the '--context' flag or update 'current_context' in the config",
cli.config.CurrentContext,
cli.config.Path(),
)
}
contextName = cli.config.CurrentContext
} }
cfg, ok := cli.config.Clusters[clusterName] cfg, ok := cli.config.Contexts[contextName]
if !ok { if !ok {
return nil, fmt.Errorf("cluster %q not found in the config", clusterName) return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
contextName, cli.config.Path())
} }
if len(cfg.Connections) == 0 { if len(cfg.Connections) == 0 {
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName) return nil, fmt.Errorf(
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
contextName, cli.config.Path(),
)
} }
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection. // TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
@@ -107,7 +115,7 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
c, err := connectCluster(ctx, conn) c, err := connectCluster(ctx, conn)
if err != nil { if err != nil {
return nil, errors.New("no valid connection configuration found for the cluster") return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
} }
return c, nil return c, nil
@@ -141,13 +149,13 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
func (cli *CLI) InitCluster( func (cli *CLI) InitCluster(
ctx context.Context, ctx context.Context,
remoteMachine *RemoteMachine, remoteMachine *RemoteMachine,
clusterName, contextName,
machineName string, machineName string,
netPrefix netip.Prefix, netPrefix netip.Prefix,
publicIP *netip.Addr, publicIP *netip.Addr,
) (*client.Client, error) { ) (*client.Client, error) {
if remoteMachine != nil { if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP) return cli.initRemoteMachine(ctx, *remoteMachine, contextName, machineName, netPrefix, publicIP)
} }
// TODO: implement local machine initialisation // TODO: implement local machine initialisation
return nil, fmt.Errorf("local machine initialisation is not implemented yet") return nil, fmt.Errorf("local machine initialisation is not implemented yet")
@@ -156,16 +164,16 @@ func (cli *CLI) InitCluster(
func (cli *CLI) initRemoteMachine( func (cli *CLI) initRemoteMachine(
ctx context.Context, ctx context.Context,
remoteMachine RemoteMachine, remoteMachine RemoteMachine,
clusterName, contextName,
machineName string, machineName string,
netPrefix netip.Prefix, netPrefix netip.Prefix,
publicIP *netip.Addr, publicIP *netip.Addr,
) (*client.Client, error) { ) (*client.Client, error) {
if clusterName == "" { if contextName == "" {
clusterName = defaultClusterName contextName = defaultContextName
} }
if _, ok := cli.config.Clusters[clusterName]; ok { if _, ok := cli.config.Contexts[contextName]; ok {
return nil, fmt.Errorf("cluster %q already exists", clusterName) return nil, fmt.Errorf("cluster %q already exists", contextName)
} }
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
@@ -207,24 +215,24 @@ func (cli *CLI) initRemoteMachine(
if err != nil { if err != nil {
return nil, fmt.Errorf("init cluster: %w", err) return nil, fmt.Errorf("init cluster: %w", err)
} }
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name) fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
resp.Machine.Name, contextName, cli.config.Path())
if err = cli.CreateCluster(clusterName); err != nil { if err = cli.CreateContext(contextName); err != nil {
return nil, fmt.Errorf("save cluster to config: %w", err) return nil, fmt.Errorf("save cluster context to config: %w", err)
} }
// Set the current cluster to the just created one if it is the only cluster in the config. // Set the current cluster to the just created one if it is the only cluster in the config.
if len(cli.config.Clusters) == 1 { if len(cli.config.Contexts) == 1 {
if err = cli.SetCurrentCluster(clusterName); err != nil { if err = cli.SetCurrentContext(contextName); err != nil {
return nil, fmt.Errorf("set current cluster: %w", err) return nil, fmt.Errorf("set current cluster context: %w", err)
} }
} }
// Save the machine's SSH connection details in the cluster config. // Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: remoteMachine.KeyPath,
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.config.Contexts[contextName].Connections = append(cli.config.Contexts[contextName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
@@ -234,11 +242,11 @@ func (cli *CLI) initRemoteMachine(
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine // AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
// which should be closed after use by the caller. // which should be closed after use by the caller.
func (cli *CLI) AddMachine( func (cli *CLI) AddMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr, ctx context.Context, remoteMachine RemoteMachine, contextName, machineName string, publicIP *netip.Addr,
) (*client.Client, error) { ) (*client.Client, error) {
c, err := cli.ConnectCluster(ctx, clusterName) c, err := cli.ConnectCluster(ctx, contextName)
if err != nil { if err != nil {
return nil, fmt.Errorf("connect to cluster: %w", err) return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
} }
defer c.Close() defer c.Close()
@@ -295,7 +303,7 @@ func (cli *CLI) AddMachine(
addResp, err := c.AddMachine(ctx, addReq) addResp, err := c.AddMachine(ctx, addReq)
if err != nil { if err != nil {
return nil, fmt.Errorf("add machine to cluster: %w", err) return nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
} }
// List other machines in the cluster to include them in the join request. // List other machines in the cluster to include them in the join request.
@@ -319,17 +327,17 @@ func (cli *CLI) AddMachine(
return nil, fmt.Errorf("join cluster: %w", err) return nil, fmt.Errorf("join cluster: %w", err)
} }
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name) fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
// Save the machine's SSH connection details in the cluster config. // Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: remoteMachine.KeyPath,
} }
if clusterName == "" { if contextName == "" {
clusterName = cli.config.CurrentCluster contextName = cli.config.CurrentContext
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.config.Contexts[contextName].Connections = append(cli.config.Contexts[contextName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
@@ -396,7 +404,8 @@ func (cli *CLI) promptResetMachine() error {
return fmt.Errorf("remote machine is already initialised as a cluster member") return fmt.Errorf("remote machine is already initialised as a cluster member")
} }
// TODO: implement resetting the remote machine. // TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet") return fmt.Errorf("resetting the remote machine is not implemented yet. " +
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
} }
// ProgressOut returns an output stream for progress writer. // ProgressOut returns an output stream for progress writer.
-6
View File
@@ -1,6 +0,0 @@
package config
type Cluster struct {
Name string `toml:"-"`
Connections []MachineConnection `toml:"connections"`
}
+21 -12
View File
@@ -2,14 +2,15 @@ package config
import ( import (
"fmt" "fmt"
"github.com/BurntSushi/toml"
"os" "os"
"path/filepath" "path/filepath"
"gopkg.in/yaml.v3"
) )
type Config struct { type Config struct {
Clusters map[string]*Cluster `toml:"clusters"` CurrentContext string `yaml:"current_context"`
CurrentCluster string `toml:"current_cluster"` Contexts map[string]*Context `yaml:"contexts"`
// path is the file path config is read from. // path is the file path config is read from.
path string path string
@@ -18,10 +19,10 @@ type Config struct {
func NewFromFile(path string) (*Config, error) { func NewFromFile(path string) (*Config, error) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("check file permissions %q: %w", path, err) return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
} }
c := &Config{ c := &Config{
Clusters: map[string]*Cluster{}, Contexts: map[string]*Context{},
path: path, path: path,
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -34,30 +35,38 @@ func NewFromFile(path string) (*Config, error) {
return c, nil return c, nil
} }
func (c *Config) Path() string {
return c.path
}
func (c *Config) Read() error { func (c *Config) Read() error {
_, err := toml.DecodeFile(c.path, c) data, err := os.ReadFile(c.path)
if err != nil { if err != nil {
return fmt.Errorf("read config file %q: %w", c.path, err) return fmt.Errorf("read config file '%s': %w", c.path, err)
} }
if err = yaml.Unmarshal(data, c); err != nil {
return fmt.Errorf("parse config file '%s': %w", c.path, err)
}
return nil return nil
} }
func (c *Config) Save() error { func (c *Config) Save() error {
dir, _ := filepath.Split(c.path) dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil { if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("create config directory %q: %w", dir, err) return fmt.Errorf("create config directory '%s': %w", dir, err)
} }
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return fmt.Errorf("write config file %q: %w", c.path, err) return fmt.Errorf("write config file '%s': %w", c.path, err)
} }
encoder := toml.NewEncoder(f) encoder := yaml.NewEncoder(f)
encoder.Indent = "" encoder.SetIndent(2)
if err = encoder.Encode(c); err != nil { if err = encoder.Encode(c); err != nil {
_ = f.Close() _ = f.Close()
return fmt.Errorf("encode config file %q: %w", c.path, err) return fmt.Errorf("encode config file '%s': %w", c.path, err)
} }
return f.Close() return f.Close()
} }
+6 -5
View File
@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
@@ -14,11 +15,11 @@ const (
) )
type MachineConnection struct { type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"` SSH SSHDestination `yaml:"ssh,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"` SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"` TCP netip.AddrPort `yaml:"tcp,omitempty"`
Host string `toml:"host,omitempty"` Host string `yaml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"` PublicKey secret.Secret `yaml:"public_key,omitempty"`
} }
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port". // SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
+6
View File
@@ -0,0 +1,6 @@
package config
type Context struct {
Name string `yaml:"-"`
Connections []MachineConnection `yaml:"connections"`
}
+12 -11
View File
@@ -2,6 +2,7 @@ package ucind
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
) )
@@ -19,11 +20,11 @@ func (u *ConfigUpdater) AddCluster(c Cluster) error {
return fmt.Errorf("read Uncloud config: %w", err) return fmt.Errorf("read Uncloud config: %w", err)
} }
if _, ok := cfg.Clusters[c.Name]; ok { if _, ok := cfg.Contexts[c.Name]; ok {
return fmt.Errorf("cluster '%s' already exists", c.Name) return fmt.Errorf("cluster context '%s' already exists", c.Name)
} }
clusterCfg := &config.Cluster{ clusterCfg := &config.Context{
Name: c.Name, Name: c.Name,
Connections: make([]config.MachineConnection, len(c.Machines)), Connections: make([]config.MachineConnection, len(c.Machines)),
} }
@@ -33,8 +34,8 @@ func (u *ConfigUpdater) AddCluster(c Cluster) error {
} }
} }
cfg.Clusters[c.Name] = clusterCfg cfg.Contexts[c.Name] = clusterCfg
cfg.CurrentCluster = c.Name cfg.CurrentContext = c.Name
if err = cfg.Save(); err != nil { if err = cfg.Save(); err != nil {
return fmt.Errorf("save config: %w", err) return fmt.Errorf("save config: %w", err)
@@ -48,17 +49,17 @@ func (u *ConfigUpdater) RemoveCluster(name string) error {
return fmt.Errorf("read Uncloud config: %w", err) return fmt.Errorf("read Uncloud config: %w", err)
} }
if _, ok := cfg.Clusters[name]; !ok { if _, ok := cfg.Contexts[name]; !ok {
return nil return nil
} }
delete(cfg.Clusters, name) delete(cfg.Contexts, name)
if cfg.CurrentCluster == name { if cfg.CurrentContext == name {
cfg.CurrentCluster = "" cfg.CurrentContext = ""
} }
if _, ok := cfg.Clusters["default"]; ok { if _, ok := cfg.Contexts["default"]; ok {
cfg.CurrentCluster = "default" cfg.CurrentContext = "default"
} }
if err = cfg.Save(); err != nil { if err = cfg.Save(); err != nil {