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"
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"
+5 -4
View File
@@ -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(),
+8 -7
View File
@@ -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)
}
+6 -5
View File
@@ -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)
}
+6 -5
View File
@@ -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)
}
+5 -4
View File
@@ -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)
}
+5 -4
View File
@@ -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)
}
+7 -6
View File
@@ -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
}
+9 -7
View File
@@ -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
}
+6 -5
View File
@@ -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
}
+3 -3
View File
@@ -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(),
+6 -5
View File
@@ -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
}
+5 -4
View File
@@ -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
}
+4 -3
View File
@@ -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
}
+5 -4
View File
@@ -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
+2 -2
View File
@@ -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
+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
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
+1 -1
View File
@@ -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
+70 -61
View File
@@ -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 clusterName == "" {
// 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 len(cli.config.Contexts) == 0 {
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,
"no cluster contexts found in the Uncloud config (%s). "+
"Please initialise a cluster with 'uncloud machine init' first",
cli.config.Path(),
)
}
clusterName = cli.config.CurrentCluster
if contextName == "" {
// If the cluster is not specified, use the current cluster if set.
if cli.config.CurrentContext == "" {
return nil, fmt.Errorf(
"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(),
)
}
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.
-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 (
"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()
}
+6 -5
View File
@@ -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".
+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 (
"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 {