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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions cmd/uc/context/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package context

import (
"context"
"fmt"
"os"
"slices"
"strings"

"github.com/goccy/go-yaml"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/spf13/cobra"
)

type createOptions struct {
context string
sshKey string
write bool
}

func NewCreateCommand() *cobra.Command {
opts := createOptions{}
cmd := &cobra.Command{
Use: "create [schema://]USER@HOST[:PORT]",
Short: "Create the cluster context to Uncloud configuration file by connecting to the remote machine.",
Long: `Create the cluster context, or add new machines to an existing cluster context.
This command adds or updates an (existing) context in your Uncloud config with machines that have a public IP address
configured. By default the context is printed to standard output, use -w to write it to the Uncloud config.

Connection methods:
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
ssh+go://user@host - Use Go's built-in SSH library`,
Example: ` # Get the cluster context with default settings.
uc context create -w root@<your-server-ip>

# Add a new context named 'prod' in the Uncloud config (~/.config/uncloud/config.yaml).
uc context create -w root@<your-server-ip> -c prod

# Add a new context with a non-root user and custom SSH port and key.
uc context create -w ubuntu@<your-server-ip>:2222 -i ~/.ssh/mykey`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)

destination := args[0]
useSSHGo := strings.HasPrefix(destination, "ssh+go://")
destination = strings.TrimPrefix(destination, "ssh+go://")
destination = strings.TrimPrefix(destination, "ssh+cli://")
destination = strings.TrimPrefix(destination, "ssh://")

if _, _, _, err := config.SSHDestination(destination).Parse(); err != nil {
return fmt.Errorf("parse remote machine: %w", err)
}

conn := config.MachineConnection{}
if useSSHGo {
conn.SSHGo = config.SSHDestination(destination)
} else {
conn.SSH = config.SSHDestination(destination)
}

return createContext(cmd.Context(), uncli, conn, opts)
},
}

cmd.Flags().StringVarP(
&opts.context, "context", "c", cli.DefaultContextName,
"Name of the new context to be created in the Uncloud config to manage the cluster.",
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
cli.DefaultSSHKeyPath),
)
cmd.Flags().BoolVarP(
&opts.write, "write", "w", false,
"Write a new Uncloud config, by default the config is only printed to standard output.",
)

return cmd
}

func createContext(ctx context.Context, uncli *cli.CLI, conn config.MachineConnection, opts createOptions) error {
contextName, err := uncli.NewContextName(opts.context)
if err != nil {
return err
}

conn.SSHKeyFile = opts.sshKey
client, err := cli.ConnectCluster(ctx, conn, cli.ConnectOptions{ShowProgress: true})
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()

machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machines = slices.DeleteFunc(machines, func(m *pb.MachineMember) bool {
return m.Machine.GetPublicIp() == nil
})

// Figure out if one of the machines is already in a context, and add the remaining there. Otherwise we
// create a new context with the name we got from the command line.
for name, context := range uncli.Config.Contexts {
for _, conn := range context.Connections {
for _, machine := range machines {
if machine.Machine.Id == conn.MachineID {
contextName = name
break
}
}
}
}

var (
user string
port int
)

if conn.SSH != "" {
user, _, port, _ = conn.SSH.Parse()
}
if conn.SSHGo != "" {
user, _, port, _ = conn.SSHGo.Parse()
}

connCfg := []config.MachineConnection{}
for _, machine := range machines {
addr, _ := machine.Machine.PublicIp.ToAddr()
dest := config.NewSSHDestination(user, addr.String(), port)

machineConn := config.MachineConnection{MachineID: machine.Machine.Id}
if conn.SSH != "" {
machineConn.SSH = dest
}
if conn.SSHGo != "" {
machineConn.SSHGo = dest
}
connCfg = append(connCfg, machineConn)
}

if !opts.write {
encoder := yaml.NewEncoder(os.Stdout, yaml.Indent(2), yaml.IndentSequence(true))
contexts := map[string]*config.Context{
contextName: {
Connections: connCfg,
},
}

encoder.Encode(contexts)
return nil
}

uncli.Config.Contexts[contextName].Connections = connCfg
if err = uncli.Config.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
}
return nil
}
1 change: 1 addition & 0 deletions cmd/uc/context/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func NewRootCommand() *cobra.Command {
NewListCommand(),
NewUseCommand(),
NewConnectionCommand(),
NewCreateCommand(),
NewShowCommand(),
)

Expand Down
6 changes: 3 additions & 3 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*clie
}

func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
contextName, err := cli.newContextName(opts.Context)
contextName, err := cli.NewContextName(opts.Context)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -289,10 +289,10 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
return machineClient, nil
}

// newContextName returns a unique name for a new cluster context. If the provided name is not DefaultContextName,
// NewContextName returns a unique name for a new cluster context. If the provided name is not DefaultContextName,
// and it's already taken, an error is returned. If the name is not provided or is DefaultContextName, the first
// available name "default[-N]" is returned.
func (cli *CLI) newContextName(name string) (string, error) {
func (cli *CLI) NewContextName(name string) (string, error) {
if name == "" {
name = DefaultContextName
}
Expand Down
1 change: 1 addition & 0 deletions website/docs/9-cli-reference/uc_ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ uc ctx [flags]

* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc ctx connection](uc_ctx_connection.md) - Choose a new default connection for the current context.
* [uc ctx create](uc_ctx_create.md) - Create the cluster context to Uncloud configuration file by connecting to the remote machine.
* [uc ctx ls](uc_ctx_ls.md) - List available cluster contexts.
* [uc ctx show](uc_ctx_show.md) - Show current cluster context.
* [uc ctx use](uc_ctx_use.md) - Switch to a different cluster context.
Expand Down
52 changes: 52 additions & 0 deletions website/docs/9-cli-reference/uc_ctx_create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# uc ctx create

Create the cluster context to Uncloud configuration file by connecting to the remote machine.

## Synopsis

Create the cluster context, or add new machines to an existing cluster context.
This command adds or updates an (existing) context in your Uncloud config with machines that have a public IP address
configured. By default the context is printed to standard output, use -w to write it to the Uncloud config.

Connection methods:
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
ssh+go://user@host - Use Go's built-in SSH library

```
uc ctx create [schema://]USER@HOST[:PORT] [flags]
```

## Examples

```
# Get the cluster context with default settings.
uc context create -w root@<your-server-ip>

# Add a new context named 'prod' in the Uncloud config (~/.config/uncloud/config.yaml).
uc context create -w root@<your-server-ip> -c prod

# Add a new context with a non-root user and custom SSH port and key.
uc context create -w ubuntu@<your-server-ip>:2222 -i ~/.ssh/mykey
```

## Options

```
-c, --context string Name of the new context to be created in the Uncloud config to manage the cluster. (default "default")
-h, --help help for create
-i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519")
-w, --write Write a new Uncloud config, by default the config is only printed to standard output.
```

## Options inherited from parent commands

```
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
Format: [ssh://]user@host[:port], ssh+go://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```

## See also

* [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts.

Loading