From f467fad84c25040831d4ff698f05f52f22a05c9b Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Tue, 1 Apr 2025 21:29:32 +1000 Subject: [PATCH] chore: rename cluster term to context in CLI and config, convert config toml -> yaml --- Makefile | 2 +- cmd/ucind/main.go | 9 ++- cmd/uncloud/caddy/deploy.go | 15 ++-- cmd/uncloud/deploy.go | 11 +-- cmd/uncloud/dns/release.go | 11 +-- cmd/uncloud/dns/reserve.go | 9 ++- cmd/uncloud/dns/show.go | 9 ++- cmd/uncloud/machine/add.go | 13 ++-- cmd/uncloud/machine/init.go | 16 ++-- cmd/uncloud/machine/list.go | 11 +-- cmd/uncloud/main.go | 6 +- cmd/uncloud/service/inspect.go | 11 +-- cmd/uncloud/service/list.go | 9 ++- cmd/uncloud/service/rm.go | 7 +- cmd/uncloud/service/run.go | 9 ++- cmd/uncloud/service/scale.go | 4 +- docs/user_guide.md | 2 +- go.mod | 2 +- internal/cli/cli.go | 125 ++++++++++++++++-------------- internal/cli/config/cluster.go | 6 -- internal/cli/config/config.go | 33 +++++--- internal/cli/config/connection.go | 11 +-- internal/cli/config/context.go | 6 ++ internal/ucind/config.go | 23 +++--- 24 files changed, 197 insertions(+), 163 deletions(-) delete mode 100644 internal/cli/config/cluster.go create mode 100644 internal/cli/config/context.go diff --git a/Makefile b/Makefile index 296414cb..4ba7a397 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ reset-dev: ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud" 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 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" diff --git a/cmd/ucind/main.go b/cmd/ucind/main.go index 9c988947..6df5c102 100644 --- a/cmd/ucind/main.go +++ b/cmd/ucind/main.go @@ -3,12 +3,13 @@ package main import ( "context" "fmt" - "github.com/docker/docker/client" - "github.com/spf13/cobra" "os" "strings" + + "github.com/docker/docker/client" "github.com/psviderski/uncloud/cmd/ucind/cluster" "github.com/psviderski/uncloud/internal/ucind" + "github.com/spf13/cobra" ) func main() { @@ -44,9 +45,9 @@ func main() { } // 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.") - _ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") + _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml") cmd.AddCommand( cluster.NewRootCommand(), diff --git a/cmd/uncloud/caddy/deploy.go b/cmd/uncloud/caddy/deploy.go index 9ca40635..b42d1a58 100644 --- a/cmd/uncloud/caddy/deploy.go +++ b/cmd/uncloud/caddy/deploy.go @@ -4,6 +4,10 @@ import ( "context" "errors" "fmt" + "maps" + "slices" + "strings" + "github.com/docker/cli/cli/streams" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" @@ -12,15 +16,12 @@ import ( "github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client/deploy" "github.com/spf13/cobra" - "maps" - "slices" - "strings" ) type deployOptions struct { image string machine string - cluster string + context string } func NewDeployCommand() *cobra.Command { @@ -42,15 +43,15 @@ func NewDeployCommand() *cobra.Command { cmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "Machine names to deploy to (comma-separated). (default is all machines)") cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster to deploy to. (default is the current cluster)", + &opts.context, "context", "c", "", + "Name of the cluster context to deploy to. (default is the current context)", ) return cmd } 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 { return fmt.Errorf("connect to cluster: %w", err) } diff --git a/cmd/uncloud/deploy.go b/cmd/uncloud/deploy.go index e5e212da..444b0c58 100644 --- a/cmd/uncloud/deploy.go +++ b/cmd/uncloud/deploy.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "strings" + "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" @@ -11,14 +13,13 @@ import ( "github.com/psviderski/uncloud/pkg/client/compose" "github.com/psviderski/uncloud/pkg/client/deploy" "github.com/spf13/cobra" - "strings" ) type deployOptions struct { files []string services []string - cluster string + context string } // 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, "One or more Compose files to deploy services from. (default compose-ports-long.yaml)") - cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "", - "Name of the cluster to deploy to (default is the current cluster)") + cmd.Flags().StringVarP(&opts.context, "context", "c", "", + "Name of the cluster context to deploy to (default is the current context)") 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 { return fmt.Errorf("connect to cluster: %w", err) } diff --git a/cmd/uncloud/dns/release.go b/cmd/uncloud/dns/release.go index 768af68b..1b250fad 100644 --- a/cmd/uncloud/dns/release.go +++ b/cmd/uncloud/dns/release.go @@ -4,15 +4,16 @@ import ( "context" "errors" "fmt" + + "github.com/psviderski/uncloud/internal/cli" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "github.com/psviderski/uncloud/internal/cli" ) type releaseOptions struct { - cluster string + context string } func NewReleaseCommand() *cobra.Command { @@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command { } cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.context, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } 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 { return fmt.Errorf("connect to cluster: %w", err) } diff --git a/cmd/uncloud/dns/reserve.go b/cmd/uncloud/dns/reserve.go index 5375c90d..9a475cd0 100644 --- a/cmd/uncloud/dns/reserve.go +++ b/cmd/uncloud/dns/reserve.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/machine/api/pb" @@ -18,7 +19,7 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1" type reserveOptions struct { endpoint string - cluster string + context string } func NewReserveCommand() *cobra.Command { @@ -36,15 +37,15 @@ func NewReserveCommand() *cobra.Command { cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint, "API endpoint for the Uncloud DNS service.") cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.context, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } 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 { return fmt.Errorf("connect to cluster: %w", err) } diff --git a/cmd/uncloud/dns/show.go b/cmd/uncloud/dns/show.go index 0c23039d..adcdc958 100644 --- a/cmd/uncloud/dns/show.go +++ b/cmd/uncloud/dns/show.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" + "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/pkg/api" "github.com/spf13/cobra" ) type showOptions struct { - cluster string + context string } func NewShowCommand() *cobra.Command { @@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command { } cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.context, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } 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 { return fmt.Errorf("connect to cluster: %w", err) } diff --git a/cmd/uncloud/machine/add.go b/cmd/uncloud/machine/add.go index 4941269c..7eb335c4 100644 --- a/cmd/uncloud/machine/add.go +++ b/cmd/uncloud/machine/add.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "net/netip" + "time" + "github.com/cenkalti/backoff/v4" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/cmd/uncloud/caddy" @@ -16,8 +19,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "net/netip" - "time" ) type addOptions struct { @@ -25,7 +26,7 @@ type addOptions struct { noCaddy bool publicIP string sshKey string - cluster string + context string } func NewAddCommand() *cobra.Command { @@ -66,8 +67,8 @@ func NewAddCommand() *cobra.Command { "path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", ) cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster to add the machine to. (default is the current cluster)", + &opts.context, "context", "c", "", + "Name of the cluster context to add the machine to. (default is the current context)", ) return cmd } @@ -87,7 +88,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o 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 { return err } diff --git a/cmd/uncloud/machine/init.go b/cmd/uncloud/machine/init.go index c6c19ac9..a5cb6807 100644 --- a/cmd/uncloud/machine/init.go +++ b/cmd/uncloud/machine/init.go @@ -3,6 +3,8 @@ package machine import ( "context" "fmt" + "net/netip" + "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/cmd/uncloud/caddy" "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/cluster" "github.com/spf13/cobra" - "net/netip" ) type initOptions struct { @@ -22,15 +23,16 @@ type initOptions struct { noDNS bool publicIP string sshKey string - cluster string + context string } func NewInitCommand() *cobra.Command { opts := initOptions{} cmd := &cobra.Command{ Use: "init [USER@HOST:PORT]", - Short: "Initialise a new cluster that consists of the local or remote machine.", - // TODO: include usage examples of initialising a local and remote machine. + Short: "Initialise a new cluster with a remote machine as the first member.", + // TODO: include usage examples of initialising a remote machine. + // TODO: support initialising a cluster on the local machine. Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { 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_*)", ) cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster in the local config if initialising a remote machine.", + &opts.context, "context", "c", "", + "Name of the cluster context in the local config.", ) return cmd @@ -107,7 +109,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM 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 { return err } diff --git a/cmd/uncloud/machine/list.go b/cmd/uncloud/machine/list.go index 80d47b16..8d24afbf 100644 --- a/cmd/uncloud/machine/list.go +++ b/cmd/uncloud/machine/list.go @@ -3,29 +3,30 @@ package machine import ( "context" "fmt" - "github.com/spf13/cobra" "net/netip" "os" "strings" "text/tabwriter" + "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/machine/network" + "github.com/spf13/cobra" ) func NewListCommand() *cobra.Command { - var cluster string + var clusterContext string cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, Short: "List machines in a cluster.", RunE: func(cmd *cobra.Command, args []string) error { uncli := cmd.Context().Value("cli").(*cli.CLI) - return list(cmd.Context(), uncli, cluster) + return list(cmd.Context(), uncli, clusterContext) }, } cmd.Flags().StringVarP( - &cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &clusterContext, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } diff --git a/cmd/uncloud/main.go b/cmd/uncloud/main.go index 4217b1af..8cf0c40e 100644 --- a/cmd/uncloud/main.go +++ b/cmd/uncloud/main.go @@ -53,7 +53,7 @@ func main() { configPath := fs.ExpandHomeDir(opts.configPath) uncli, err := cli.New(configPath, conn) 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)) return nil @@ -64,9 +64,9 @@ func main() { "Connect to a remote cluster machine without using the Uncloud configuration file.\n"+ "Format: [ssh://]user@host[:port] or tcp://host:port") // 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.") - _ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") + _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml") cmd.AddCommand( NewDeployCommand(), diff --git a/cmd/uncloud/service/inspect.go b/cmd/uncloud/service/inspect.go index 5668e119..f0706db4 100644 --- a/cmd/uncloud/service/inspect.go +++ b/cmd/uncloud/service/inspect.go @@ -3,14 +3,15 @@ package service import ( "context" "fmt" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/go-units" "os" "text/tabwriter" "time" - "github.com/spf13/cobra" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-units" + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" ) type inspectOptions struct { @@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command { }, } cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.cluster, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } diff --git a/cmd/uncloud/service/list.go b/cmd/uncloud/service/list.go index 73a34577..e876038a 100644 --- a/cmd/uncloud/service/list.go +++ b/cmd/uncloud/service/list.go @@ -3,11 +3,12 @@ package service import ( "context" "fmt" - "github.com/psviderski/uncloud/internal/cli" - "github.com/spf13/cobra" "os" "strings" "text/tabwriter" + + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" ) func NewListCommand() *cobra.Command { @@ -22,8 +23,8 @@ func NewListCommand() *cobra.Command { }, } cmd.Flags().StringVarP( - &cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &cluster, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } diff --git a/cmd/uncloud/service/rm.go b/cmd/uncloud/service/rm.go index 572365e8..cbbbefcd 100644 --- a/cmd/uncloud/service/rm.go +++ b/cmd/uncloud/service/rm.go @@ -3,11 +3,12 @@ package service import ( "context" "fmt" + "os" + "github.com/docker/cli/cli/streams" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" "github.com/spf13/cobra" - "os" ) type rmOptions struct { @@ -29,8 +30,8 @@ func NewRmCommand() *cobra.Command { }, } cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.cluster, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd } diff --git a/cmd/uncloud/service/run.go b/cmd/uncloud/service/run.go index 6fb49ea2..b22febf5 100644 --- a/cmd/uncloud/service/run.go +++ b/cmd/uncloud/service/run.go @@ -3,13 +3,14 @@ package service import ( "context" "fmt" + "slices" + "strings" + "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client/deploy" "github.com/spf13/cobra" - "slices" - "strings" ) type runOptions struct { @@ -79,8 +80,8 @@ func NewRunCommand() *cobra.Command { "/host/path:/container/path[:ro]. Can be specified multiple times.") cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster to run the service in. (default is the current cluster)", + &opts.cluster, "context", "c", "", + "Name of the cluster context to run the service in. (default is the current context)", ) return cmd diff --git a/cmd/uncloud/service/scale.go b/cmd/uncloud/service/scale.go index 80c55ebf..222f10ea 100644 --- a/cmd/uncloud/service/scale.go +++ b/cmd/uncloud/service/scale.go @@ -40,8 +40,8 @@ func NewScaleCommand() *cobra.Command { } cmd.Flags().StringVarP( - &opts.cluster, "cluster", "c", "", - "Name of the cluster. (default is the current cluster)", + &opts.cluster, "context", "c", "", + "Name of the cluster context. (default is the current context)", ) return cmd diff --git a/docs/user_guide.md b/docs/user_guide.md index b09bf337..34ba05f4 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -16,7 +16,7 @@ In addition, any ports for any workloads you want to expose will need to be open ### 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 diff --git a/go.mod b/go.mod index 48162c34..32856a52 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.36.3 + gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.36.3 ) @@ -310,7 +311,6 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // 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 howett.net/plist v1.0.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect diff --git a/internal/cli/cli.go b/internal/cli/cli.go index ee73c384..cf720a53 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "net/netip" + "os" + "github.com/docker/cli/cli/streams" "github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/fs" @@ -13,14 +16,12 @@ import ( "github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client/connector" - "net/netip" - "os" "github.com/charmbracelet/huh" "google.golang.org/protobuf/types/known/emptypb" ) -const defaultClusterName = "default" +const defaultContextName = "default" type CLI struct { config *config.Config @@ -45,61 +46,68 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) { }, nil } -func (cli *CLI) CreateCluster(name string) error { - if _, ok := cli.config.Clusters[name]; ok { - return fmt.Errorf("cluster %q already exists", name) +func (cli *CLI) CreateContext(name string) error { + if _, ok := cli.config.Contexts[name]; ok { + return fmt.Errorf("context '%s' already exists", name) } - cli.config.Clusters[name] = &config.Cluster{ + cli.config.Contexts[name] = &config.Context{ Name: name, } return cli.config.Save() } -func (cli *CLI) SetCurrentCluster(name string) error { - if _, ok := cli.config.Clusters[name]; !ok { +func (cli *CLI) SetCurrentContext(name string) error { + if _, ok := cli.config.Contexts[name]; !ok { return api.ErrNotFound } - cli.config.CurrentCluster = name + cli.config.CurrentContext = name 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. -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 { return connectCluster(ctx, *cli.conn) } - if len(cli.config.Clusters) == 0 { - return nil, errors.New( - "no clusters found in the Uncloud config. " + - "Please initialise a cluster with `uncloud machine init` first", + if len(cli.config.Contexts) == 0 { + return nil, fmt.Errorf( + "no cluster contexts found in the Uncloud config (%s). "+ + "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 cli.config.CurrentCluster == "" { - 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 { + if cli.config.CurrentContext == "" { return nil, fmt.Errorf( - "current cluster %q not found in the config. "+ - "Please specify a cluster with the --cluster flag or update current_cluster in the config", - cli.config.CurrentCluster, + "the current cluster context is not set in the Uncloud config (%s). "+ + "Please specify the context with the '--context' flag or set 'current_context' in the config", + 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 { - 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 { - 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. @@ -107,7 +115,7 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client c, err := connectCluster(ctx, conn) 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 @@ -141,13 +149,13 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client func (cli *CLI) InitCluster( ctx context.Context, remoteMachine *RemoteMachine, - clusterName, + contextName, machineName string, netPrefix netip.Prefix, publicIP *netip.Addr, ) (*client.Client, error) { 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 return nil, fmt.Errorf("local machine initialisation is not implemented yet") @@ -156,16 +164,16 @@ func (cli *CLI) InitCluster( func (cli *CLI) initRemoteMachine( ctx context.Context, remoteMachine RemoteMachine, - clusterName, + contextName, machineName string, netPrefix netip.Prefix, publicIP *netip.Addr, ) (*client.Client, error) { - if clusterName == "" { - clusterName = defaultClusterName + if contextName == "" { + contextName = defaultContextName } - if _, ok := cli.config.Clusters[clusterName]; ok { - return nil, fmt.Errorf("cluster %q already exists", clusterName) + if _, ok := cli.config.Contexts[contextName]; ok { + return nil, fmt.Errorf("cluster %q already exists", contextName) } machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) @@ -207,24 +215,24 @@ func (cli *CLI) initRemoteMachine( if err != nil { return nil, fmt.Errorf("init cluster: %w", err) } - fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name) - - if err = cli.CreateCluster(clusterName); err != nil { - return nil, fmt.Errorf("save cluster to config: %w", err) + 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.CreateContext(contextName); err != nil { + 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. - if len(cli.config.Clusters) == 1 { - if err = cli.SetCurrentCluster(clusterName); err != nil { - return nil, fmt.Errorf("set current cluster: %w", err) + if len(cli.config.Contexts) == 1 { + if err = cli.SetCurrentContext(contextName); err != nil { + 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{ SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), 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 { 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 // which should be closed after use by the caller. 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) { - c, err := cli.ConnectCluster(ctx, clusterName) + c, err := cli.ConnectCluster(ctx, contextName) 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() @@ -295,7 +303,7 @@ func (cli *CLI) AddMachine( addResp, err := c.AddMachine(ctx, addReq) 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. @@ -319,17 +327,17 @@ func (cli *CLI) AddMachine( 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{ SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSHKeyFile: remoteMachine.KeyPath, } - if clusterName == "" { - clusterName = cli.config.CurrentCluster + if contextName == "" { + 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 { 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") } // 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. diff --git a/internal/cli/config/cluster.go b/internal/cli/config/cluster.go deleted file mode 100644 index 72febcb4..00000000 --- a/internal/cli/config/cluster.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type Cluster struct { - Name string `toml:"-"` - Connections []MachineConnection `toml:"connections"` -} diff --git a/internal/cli/config/config.go b/internal/cli/config/config.go index 942aa988..98dd70eb 100644 --- a/internal/cli/config/config.go +++ b/internal/cli/config/config.go @@ -2,14 +2,15 @@ package config import ( "fmt" - "github.com/BurntSushi/toml" "os" "path/filepath" + + "gopkg.in/yaml.v3" ) type Config struct { - Clusters map[string]*Cluster `toml:"clusters"` - CurrentCluster string `toml:"current_cluster"` + CurrentContext string `yaml:"current_context"` + Contexts map[string]*Context `yaml:"contexts"` // path is the file path config is read from. path string @@ -18,10 +19,10 @@ type Config struct { func NewFromFile(path string) (*Config, error) { _, err := os.Stat(path) 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{ - Clusters: map[string]*Cluster{}, + Contexts: map[string]*Context{}, path: path, } if os.IsNotExist(err) { @@ -34,30 +35,38 @@ func NewFromFile(path string) (*Config, error) { return c, nil } +func (c *Config) Path() string { + return c.path +} + func (c *Config) Read() error { - _, err := toml.DecodeFile(c.path, c) + data, err := os.ReadFile(c.path) 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 } func (c *Config) Save() error { dir, _ := filepath.Split(c.path) 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) 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.Indent = "" + encoder := yaml.NewEncoder(f) + encoder.SetIndent(2) if err = encoder.Encode(c); err != nil { _ = 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() } diff --git a/internal/cli/config/connection.go b/internal/cli/config/connection.go index b84ef10f..2fb46c7f 100644 --- a/internal/cli/config/connection.go +++ b/internal/cli/config/connection.go @@ -5,6 +5,7 @@ import ( "net/netip" "strconv" "strings" + "github.com/psviderski/uncloud/internal/secret" ) @@ -14,11 +15,11 @@ const ( ) type MachineConnection struct { - SSH SSHDestination `toml:"ssh,omitempty"` - SSHKeyFile string `toml:"ssh_key_file,omitempty"` - TCP netip.AddrPort `toml:"tcp,omitempty"` - Host string `toml:"host,omitempty"` - PublicKey secret.Secret `toml:"public_key,omitempty"` + SSH SSHDestination `yaml:"ssh,omitempty"` + SSHKeyFile string `yaml:"ssh_key_file,omitempty"` + TCP netip.AddrPort `yaml:"tcp,omitempty"` + Host string `yaml:"host,omitempty"` + PublicKey secret.Secret `yaml:"public_key,omitempty"` } // SSHDestination represents an SSH destination string in the canonical form of "user@host:port". diff --git a/internal/cli/config/context.go b/internal/cli/config/context.go new file mode 100644 index 00000000..a86b53da --- /dev/null +++ b/internal/cli/config/context.go @@ -0,0 +1,6 @@ +package config + +type Context struct { + Name string `yaml:"-"` + Connections []MachineConnection `yaml:"connections"` +} diff --git a/internal/ucind/config.go b/internal/ucind/config.go index 7d147d90..5136b205 100644 --- a/internal/ucind/config.go +++ b/internal/ucind/config.go @@ -2,6 +2,7 @@ package ucind import ( "fmt" + "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) } - if _, ok := cfg.Clusters[c.Name]; ok { - return fmt.Errorf("cluster '%s' already exists", c.Name) + if _, ok := cfg.Contexts[c.Name]; ok { + return fmt.Errorf("cluster context '%s' already exists", c.Name) } - clusterCfg := &config.Cluster{ + clusterCfg := &config.Context{ Name: c.Name, Connections: make([]config.MachineConnection, len(c.Machines)), } @@ -33,8 +34,8 @@ func (u *ConfigUpdater) AddCluster(c Cluster) error { } } - cfg.Clusters[c.Name] = clusterCfg - cfg.CurrentCluster = c.Name + cfg.Contexts[c.Name] = clusterCfg + cfg.CurrentContext = c.Name if err = cfg.Save(); err != nil { 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) } - if _, ok := cfg.Clusters[name]; !ok { + if _, ok := cfg.Contexts[name]; !ok { return nil } - delete(cfg.Clusters, name) + delete(cfg.Contexts, name) - if cfg.CurrentCluster == name { - cfg.CurrentCluster = "" + if cfg.CurrentContext == name { + cfg.CurrentContext = "" } - if _, ok := cfg.Clusters["default"]; ok { - cfg.CurrentCluster = "default" + if _, ok := cfg.Contexts["default"]; ok { + cfg.CurrentContext = "default" } if err = cfg.Save(); err != nil {