Compare commits

..
21 Commits
Author SHA1 Message Date
Pasha Sviderski d5687cd713 chore: combine some e2e tests to reduce the number of ucind clusters hence used system resources 2026-03-09 19:07:00 +10:00
Pasha Sviderski a67f936cc1 chore: wait for known missing changes during the initial cluster store sync 2026-03-09 17:40:51 +10:00
Pasha Sviderski 51b8c29d63 fix: typo 2026-03-09 17:38:37 +10:00
Pasha Sviderski e6f013dbc8 fix: skip Tailscale addresses for using as auto-detected WireGuard endpoints 2026-03-09 17:38:25 +10:00
Pasha Sviderski 897f03aafd chore: skip not found services when streaming logs for Compose services (print warning) 2026-03-09 09:33:33 +10:00
Pasha Sviderski 7b306f25ce fix: another attempt to fix flaky test container_crashes_on_startup_without_healthcheck 2026-03-06 21:12:23 +10:00
Pasha Sviderski 78a3551c77 docs: regenerate CLI reference 2026-03-06 21:01:38 +10:00
Pasha Sviderski 38c7dda6a0 chore: rename --endpoint to --wg-endpoint for 'uc machine update' command 2026-03-06 15:45:13 +10:00
Pasha Sviderski 51c7b5ba8f feat: add --wg-endpoint flag to 'uc machine add/init' to specify wireguard endpoint for the machine instead of auto-detecting 2026-03-06 15:39:14 +10:00
GabrielandGitHub a507b202ec fix: create AAAA DNS record for IPv6 machines" (#257)
* chore: update gitignore
* fix: create AAAA DNS record for IPv6 machines"
* test: randomize execution order to prevent order-based bugs in tests
2026-03-04 15:17:27 +10:00
Pasha Sviderski d9934b9dba feat: support compose stop_grace_period to change timeout before sending SIGKILL 2026-03-04 14:07:22 +10:00
Pasha Sviderski 1f328e99d5 fix: remove the incorrect e2e test for updating machine endpoints with empty list 2026-03-04 14:04:46 +10:00
Pasha Sviderski 3c435434a8 fix: flaky test TestDeployment/container_crashes_on_startup_without_healthcheck 2026-03-04 14:03:29 +10:00
Pasha Sviderski e54871758b chore: increase wait for test cluster timeout 30s -> 60s for e2e tests on GHA 2026-03-04 13:07:09 +10:00
Pasha Sviderski c039390ade docs: regenerate CLI reference 2026-03-04 12:47:21 +10:00
Pasha Sviderski bad0f97b66 feat: add support for updating WireGuard endpoints for a machine (uc machine update) 2026-03-03 20:21:27 +10:00
Pasha Sviderski 2405b156e4 chore: validate endpoints is not empty in update machine requests 2026-03-03 19:04:41 +10:00
Pasha Sviderski 4de17cb63c chore: delete the obsolete 'machine token' command 2026-03-03 18:03:06 +10:00
Pasha Sviderski 2222434934 fix: connection formatting when changing connection with 'ctx conn' 2026-03-03 17:26:42 +10:00
Pasha Sviderski 3f1a772288 docs: minor polish for rolling deployments doc 2026-03-03 16:39:43 +10:00
Pasha Sviderski 7b9bed0780 docs: update demo thumbnail in README 2026-03-03 15:01:59 +10:00
40 changed files with 999 additions and 756 deletions
+2
View File
@@ -40,3 +40,5 @@ node_modules/
# VS Code # VS Code
.vscode .vscode
.devcontainer/ .devcontainer/
.zed/
+3 -3
View File
@@ -73,14 +73,14 @@ mocks:
.PHONY: test .PHONY: test
test: test:
ifeq ($(TEST_NAME),) ifeq ($(TEST_NAME),)
go test -count=1 -v ./... go test -shuffle=on -count=1 -v ./...
else else
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./... go test -shuffle=on -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
endif endif
.PHONY: test-e2e .PHONY: test-e2e
test-e2e: test-e2e:
go test -race -count=1 -v ./test/e2e go test -shuffle=on -race -count=1 -v ./test/e2e
.PHONY: test-clean .PHONY: test-clean
test-clean: test-clean:
+1 -1
View File
@@ -61,7 +61,7 @@ Here is a more advanced use case. Deploy a highly available web app with automat
on-premises in just a couple minutes. on-premises in just a couple minutes.
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau"> <a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" /> <img src="https://embed-ssl.wistia.com/deliveries/4fb71ea442905b81b1ad4e25fc02132e.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
</a> </a>
<br> <br>
+1 -1
View File
@@ -60,7 +60,7 @@ func selectConnection(uncli *cli.CLI) error {
return fmt.Errorf("save config: %w", err) return fmt.Errorf("save config: %w", err)
} }
fmt.Printf("Default connection for context '%s' is now '%s'.\n", currentCtxName, selected) fmt.Printf("Default connection for context '%s' is now '%s'.\n", currentCtxName, selected.String())
return nil return nil
} }
+29 -9
View File
@@ -14,19 +14,21 @@ import (
"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/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/network"
"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/spf13/cobra" "github.com/spf13/cobra"
) )
type addOptions struct { type addOptions struct {
name string name string
noCaddy bool noCaddy bool
noInstall bool noInstall bool
publicIP string publicIP string
sshKey string sshKey string
version string version string
yes bool wgEndpoints []string
yes bool
} }
func NewAddCommand() *cobra.Command { func NewAddCommand() *cobra.Command {
@@ -90,6 +92,14 @@ Connection methods:
&opts.version, "version", "latest", &opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.", "Version of the Uncloud daemon to install on the machine.",
) )
cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil,
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
"Default port %d is used if omitted.\n", network.WireGuardPort)+
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
"If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.",
)
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+ "Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]") "Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
@@ -112,14 +122,24 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
publicIP = &ip publicIP = &ip
} }
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{ addOpts := cli.AddMachineOptions{
MachineName: opts.name, MachineName: opts.name,
PublicIP: publicIP, PublicIP: publicIP,
RemoteMachine: remoteMachine, RemoteMachine: remoteMachine,
SkipInstall: opts.noInstall, SkipInstall: opts.noInstall,
Version: opts.version, Version: opts.version,
AutoConfirm: opts.yes, AutoConfirm: opts.yes,
}) }
if len(opts.wgEndpoints) > 0 {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil {
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
}
addOpts.WireguardEndpoints = endpoints
}
clusterClient, machineClient, err := uncli.AddMachine(ctx, addOpts)
if err != nil { if err != nil {
return err return err
} }
+23 -3
View File
@@ -16,11 +16,13 @@ import (
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"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/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type initOptions struct { type initOptions struct {
context string
dnsEndpoint string dnsEndpoint string
name string name string
network string network string
@@ -30,7 +32,7 @@ type initOptions struct {
publicIP string publicIP string
sshKey string sshKey string
version string version string
context string wgEndpoints []string
yes bool yes bool
} }
@@ -130,6 +132,14 @@ Connection methods:
&opts.version, "version", "latest", &opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.", "Version of the Uncloud daemon to install on the machine.",
) )
cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil,
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
"Default port %d is used if omitted.\n", network.WireGuardPort)+
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
"If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.",
)
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+ "Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]") "Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
@@ -161,7 +171,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
publicIP = &ip publicIP = &ip
} }
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{ initOpts := cli.InitClusterOptions{
Context: opts.context, Context: opts.context,
MachineName: opts.name, MachineName: opts.name,
Network: netPrefix, Network: netPrefix,
@@ -170,7 +180,17 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
SkipInstall: opts.noInstall, SkipInstall: opts.noInstall,
Version: opts.version, Version: opts.version,
AutoConfirm: opts.yes, AutoConfirm: opts.yes,
}) }
if len(opts.wgEndpoints) > 0 {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil {
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
}
initOpts.WireguardEndpoints = endpoints
}
client, err := uncli.InitCluster(ctx, initOpts)
if err != nil { if err != nil {
return err return err
} }
-1
View File
@@ -17,7 +17,6 @@ func NewRootCommand() *cobra.Command {
NewRenameCommand(), NewRenameCommand(),
NewRmCommand(), NewRmCommand(),
NewUpdateCommand(), NewUpdateCommand(),
NewTokenCommand(),
) )
return cmd return cmd
} }
-39
View File
@@ -1,39 +0,0 @@
package machine
import (
"fmt"
"github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/machine"
"github.com/spf13/cobra"
)
type tokenOptions struct {
dataDir string
}
func NewTokenCommand() *cobra.Command {
opts := tokenOptions{}
cmd := &cobra.Command{
Use: "token",
Short: "Print the local machine's token for adding it to a cluster.",
RunE: func(cmd *cobra.Command, args []string) error {
token, err := daemon.MachineToken(opts.dataDir)
if err != nil {
return fmt.Errorf("get machine token: %w", err)
}
tokenStr, err := token.String()
if err != nil {
return fmt.Errorf("encode machine token: %w", err)
}
fmt.Println(tokenStr)
return nil
},
}
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
"Directory for storing persistent machine state.")
_ = cmd.MarkFlagDirname("data-dir")
return cmd
}
+63 -12
View File
@@ -4,29 +4,43 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"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/internal/machine/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type updateOptions struct { type updateOptions struct {
name string name string
publicIP string publicIP string
wgEndpoints []string
} }
func NewUpdateCommand() *cobra.Command { func NewUpdateCommand() *cobra.Command {
opts := updateOptions{} opts := updateOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update", Use: "update MACHINE [flags]",
Short: "Update machine configuration in the cluster.", Short: "Update machine configuration in the cluster.",
Long: `Update machine configuration in the cluster. Long: `Update machine configuration in the cluster.
This command allows setting various machine properties including: Change the name, public IP address, or WireGuard endpoints of an existing machine.
- Machine name (--name) At least one flag must be specified to perform an update.`,
- Public IP address (--public-ip) Example: ` # Rename a machine.
uc machine update machine1 --name web-server
At least one flag must be specified to perform an update operation.`, # Set the public IP address of a machine.
uc machine update machine1 --public-ip 203.0.113.10
# Remove the public IP address from a machine.
uc machine update machine1 --public-ip none
# Update WireGuard endpoints for a machine.
uc machine update machine1 --wg-endpoint 203.0.113.10 --wg-endpoint 192.168.1.5
# Update multiple properties at once.
uc machine update machine1 --name web-server --public-ip 203.0.113.10`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(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)
@@ -40,16 +54,24 @@ At least one flag must be specified to perform an update operation.`,
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "", &opts.publicIP, "public-ip", "",
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone), fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.",
PublicIPNone),
)
cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil,
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
"Default port %d is used if omitted.\n", network.WireGuardPort)+
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.",
) )
return cmd return cmd
} }
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error { func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
// Check if at least one flag was explicitly set // Check if at least one flag was explicitly set.
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") { if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") && !cmd.Flags().Changed("wg-endpoint") {
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)") return fmt.Errorf("at least one update flag must be specified (--name, --public-ip, --wg-endpoint)")
} }
client, err := uncli.ConnectCluster(ctx) client, err := uncli.ConnectCluster(ctx)
@@ -87,6 +109,19 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
} }
} }
// Parse and set endpoints if the flag was explicitly provided.
if cmd.Flags().Changed("wg-endpoint") {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil {
return err
}
if len(endpoints) == 0 {
return fmt.Errorf("at least one endpoint must be specified if --wg-endpoint flag is used")
}
req.Endpoints = endpoints
}
// Perform the update operation // Perform the update operation
updatedMachine, err := client.UpdateMachine(ctx, req) updatedMachine, err := client.UpdateMachine(ctx, req)
if err != nil { if err != nil {
@@ -113,8 +148,24 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
} }
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP)) changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
} }
if cmd.Flags().Changed("wg-endpoint") {
formatEndpoints := func(eps []*pb.IPPort) string {
if len(eps) == 0 {
return "none"
}
parts := make([]string, len(eps))
for i, ep := range eps {
ap, _ := ep.ToAddrPort()
parts[i] = ap.String()
}
return strings.Join(parts, ", ")
}
oldEndpoints := formatEndpoints(machine.Machine.Network.Endpoints)
newEndpoints := formatEndpoints(updatedMachine.Network.Endpoints)
changes = append(changes, fmt.Sprintf("endpoints: %s -> %s", oldEndpoints, newEndpoints))
}
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id) fmt.Printf("Machine '%s' (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
for _, change := range changes { for _, change := range changes {
fmt.Printf(" %s\n", change) fmt.Printf(" %s\n", change)
} }
+25 -3
View File
@@ -99,15 +99,17 @@ If no services are specified, streams logs from all services defined in the Comp
func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts logsOptions) error { func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts logsOptions) error {
// If no services specified, try to load them from the Compose file(s). // If no services specified, try to load them from the Compose file(s).
fromCompose := false
if len(serviceNames) == 0 { if len(serviceNames) == 0 {
fromCompose = true
project, err := compose.LoadProject(ctx, opts.files) project, err := compose.LoadProject(ctx, opts.files)
if err != nil { if err != nil {
return fmt.Errorf("load compose file(s): %w", err) return fmt.Errorf("load Compose file(s): %w", err)
} }
// View logs for all services, including disabled by inactive profiles. // View logs for all services, including disabled by inactive profiles.
serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...) serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...)
if len(serviceNames) == 0 { if len(serviceNames) == 0 {
return errors.New("no services found in compose file(s)") return errors.New("no services found in Compose file(s)")
} }
} }
@@ -135,20 +137,40 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
Machines: cli.ExpandCommaSeparatedValues(opts.machines), Machines: cli.ExpandCommaSeparatedValues(opts.machines),
} }
// Collect log streams from all services. // Collect log streams from all services. When service names come from a Compose file,
// skip the ones that are not found in the cluster (they may have been removed or not deployed yet).
machineIDsSet := mapset.NewSet[string]() machineIDsSet := mapset.NewSet[string]()
svcStreams := make([]<-chan api.ServiceLogEntry, 0, len(serviceNames)) svcStreams := make([]<-chan api.ServiceLogEntry, 0, len(serviceNames))
var foundServices, notFoundServices []string
for _, serviceName := range serviceNames { for _, serviceName := range serviceNames {
svc, ch, err := c.ServiceLogs(ctx, serviceName, logsOpts) svc, ch, err := c.ServiceLogs(ctx, serviceName, logsOpts)
if err != nil { if err != nil {
if errors.Is(err, api.ErrNotFound) && fromCompose {
notFoundServices = append(notFoundServices, serviceName)
continue
}
return fmt.Errorf("stream logs for service '%s': %w", serviceName, err) return fmt.Errorf("stream logs for service '%s': %w", serviceName, err)
} }
svcStreams = append(svcStreams, ch) svcStreams = append(svcStreams, ch)
foundServices = append(foundServices, serviceName)
machineIDs := svc.MachineIDs() machineIDs := svc.MachineIDs()
machineIDsSet.Append(machineIDs...) machineIDsSet.Append(machineIDs...)
} }
if fromCompose {
if len(foundServices) == 0 {
return fmt.Errorf("stream logs for services defined in %s: no services found in the cluster",
strings.Join(opts.files, ", "))
}
serviceNames = foundServices
for _, name := range notFoundServices {
client.PrintWarning(fmt.Sprintf("service '%s' not found in the cluster, skipping", name))
}
}
var stream <-chan api.ServiceLogEntry var stream <-chan api.ServiceLogEntry
if len(serviceNames) == 1 { if len(serviceNames) == 1 {
stream = svcStreams[0] stream = svcStreams[0]
+29 -20
View File
@@ -155,14 +155,15 @@ func (cli *CLI) ConnectClusterWithOptions(ctx context.Context, opts ConnectOptio
} }
type InitClusterOptions struct { type InitClusterOptions struct {
Context string Context string
MachineName string MachineName string
Network netip.Prefix Network netip.Prefix
PublicIP *netip.Addr PublicIP *netip.Addr
RemoteMachine *RemoteMachine RemoteMachine *RemoteMachine
SkipInstall bool SkipInstall bool
Version string Version string
AutoConfirm bool AutoConfirm bool
WireguardEndpoints []*pb.IPPort
} }
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster. // InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
@@ -220,8 +221,9 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
} }
req := &pb.InitClusterRequest{ req := &pb.InitClusterRequest{
MachineName: opts.MachineName, MachineName: opts.MachineName,
Network: pb.NewIPPrefix(opts.Network), Network: pb.NewIPPrefix(opts.Network),
WireguardEndpoints: opts.WireguardEndpoints,
} }
if opts.PublicIP != nil { if opts.PublicIP != nil {
if opts.PublicIP.IsValid() { if opts.PublicIP.IsValid() {
@@ -298,12 +300,13 @@ func (cli *CLI) newContextName(name string) (string, error) {
} }
type AddMachineOptions struct { type AddMachineOptions struct {
MachineName string MachineName string
PublicIP *netip.Addr PublicIP *netip.Addr
RemoteMachine *RemoteMachine RemoteMachine *RemoteMachine
SkipInstall bool SkipInstall bool
Version string Version string
AutoConfirm bool AutoConfirm bool
WireguardEndpoints []*pb.IPPort
} }
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client. // AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
@@ -380,10 +383,16 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
return nil, nil, fmt.Errorf("parse remote machine token: %w", err) return nil, nil, fmt.Errorf("parse remote machine token: %w", err)
} }
// Register the machine in the cluster using its public key and endpoints from the token. // Register the machine in the cluster using its public key and WireGuard endpoints from the token,
endpoints := make([]*pb.IPPort, len(token.Endpoints)) // or use the explicitly provided WireGuard endpoints if any.
for i, addrPort := range token.Endpoints { var endpoints []*pb.IPPort
endpoints[i] = pb.NewIPPort(addrPort) if len(opts.WireguardEndpoints) > 0 {
endpoints = opts.WireguardEndpoints
} else {
endpoints = make([]*pb.IPPort, len(token.Endpoints))
for i, addrPort := range token.Endpoints {
endpoints[i] = pb.NewIPPort(addrPort)
}
} }
addReq := &pb.AddMachineRequest{ addReq := &pb.AddMachineRequest{
Name: opts.MachineName, Name: opts.MachineName,
+23
View File
@@ -1,10 +1,14 @@
package cli package cli
import ( import (
"fmt"
"log" "log"
"net/netip"
"os" "os"
"strings" "strings"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -34,3 +38,22 @@ func BindEnvToFlag(cmd *cobra.Command, flagName, envVar string) {
} }
} }
} }
// ParseWireGuardEndpoints parses a list of endpoint strings into a list of IPPort protobuf messages. Each value can
// be an IP address, IP:PORT, IPv6, or [IPv6]:PORT. If the port is omitted, the default WireGuard port is used.
func ParseWireGuardEndpoints(values []string) ([]*pb.IPPort, error) {
endpoints := make([]*pb.IPPort, 0, len(values))
for _, v := range values {
ap, err := netip.ParseAddrPort(v)
if err != nil {
// Try parsing as a bare IP address and use the default WireGuard port.
addr, addrErr := netip.ParseAddr(v)
if addrErr != nil {
return nil, fmt.Errorf("invalid endpoint '%s': must be IP, IPv6, IP:PORT, or [IPv6]:PORT", v)
}
ap = netip.AddrPortFrom(addr, network.WireGuardPort)
}
endpoints = append(endpoints, pb.NewIPPort(ap))
}
return endpoints, nil
}
-42
View File
@@ -1,42 +0,0 @@
package daemon
import (
"errors"
"fmt"
"net/netip"
"os"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/network"
)
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
// TODO: ideally, this should be an RPC call to the daemon API to ensure the config is created and up-to-date.
func MachineToken(dataDir string) (machine.Token, error) {
state, err := machine.ParseState(machine.StatePath(dataDir))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return machine.Token{}, fmt.Errorf("load machine config (is uncloudd daemon running?): %w", err)
}
return machine.Token{}, fmt.Errorf("load machine config: %w", err)
}
if len(state.Network.PublicKey) == 0 {
return machine.Token{}, errors.New("public key is not set in machine config")
}
ips, err := network.ListRoutableIPs()
if err != nil {
return machine.Token{}, fmt.Errorf("list routable addresses: %w", err)
}
publicIP, err := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services.
if err == nil {
ips = append(ips, publicIP)
}
endpoints := make([]netip.AddrPort, len(ips))
for i, ip := range ips {
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
}
return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
}
+169 -155
View File
@@ -233,6 +233,8 @@ type InitClusterRequest struct {
// *InitClusterRequest_PublicIp // *InitClusterRequest_PublicIp
// *InitClusterRequest_PublicIpAuto // *InitClusterRequest_PublicIpAuto
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"` PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
// Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones.
WireguardEndpoints []*IPPort `protobuf:"bytes,5,rep,name=wireguard_endpoints,json=wireguardEndpoints,proto3" json:"wireguard_endpoints,omitempty"`
} }
func (x *InitClusterRequest) Reset() { func (x *InitClusterRequest) Reset() {
@@ -302,6 +304,13 @@ func (x *InitClusterRequest) GetPublicIpAuto() bool {
return false return false
} }
func (x *InitClusterRequest) GetWireguardEndpoints() []*IPPort {
if x != nil {
return x.WireguardEndpoints
}
return nil
}
type isInitClusterRequest_PublicIpConfig interface { type isInitClusterRequest_PublicIpConfig interface {
isInitClusterRequest_PublicIpConfig() isInitClusterRequest_PublicIpConfig()
} }
@@ -1041,7 +1050,7 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69,
0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x81, 0x02, 0x0a, 0x12,
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
@@ -1052,130 +1061,134 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x69, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52,
0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x12, 0x3c, 0x0a,
0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x13, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f,
0x67, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x64, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22,
0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x2f,
0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62, 0x5f, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6d, 0x69,
0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
0x49, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x29, 0x0a,
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08,
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x25,
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x6f, 0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x6d, 0x69, 0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
0x6e, 0x22, 0x49, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
0x6c, 0x73, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x64, 0x62, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x52, 0x0e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12,
0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a,
0x15, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e,
0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65,
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72,
0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50,
0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61,
0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02,
0x0a, 0x0d, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12,
0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a,
0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61,
0x73, 0x74, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
0x6b, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76,
0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72,
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74,
0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74,
0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70,
0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
0x49, 0x70, 0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75,
0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40,
0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69,
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65,
0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f,
0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e,
0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18,
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72,
0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64,
0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x0d,
0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1d, 0x0a,
0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74,
0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f,
0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x63,
0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61,
0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73,
0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18,
0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a,
0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69,
0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73,
0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b,
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e,
0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33,
0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75,
0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x73, 0x65,
0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -1223,39 +1236,40 @@ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
18, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort 18, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
17, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix 17, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
16, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP 16, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo 18, // 7: api.InitClusterRequest.wireguard_endpoints:type_name -> api.IPPort
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo 0, // 8: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo 0, // 9: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
7, // 10: api.InspectMachineResponse.machines:type_name -> api.MachineDetails 0, // 10: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
19, // 11: api.MachineDetails.metadata:type_name -> api.Metadata 7, // 11: api.InspectMachineResponse.machines:type_name -> api.MachineDetails
0, // 12: api.MachineDetails.machine:type_name -> api.MachineInfo 19, // 12: api.MachineDetails.metadata:type_name -> api.Metadata
15, // 13: api.Service.containers:type_name -> api.Service.Container 0, // 13: api.MachineDetails.machine:type_name -> api.MachineInfo
10, // 14: api.InspectServiceResponse.service:type_name -> api.Service 15, // 14: api.Service.containers:type_name -> api.Service.Container
14, // 15: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer 10, // 15: api.InspectServiceResponse.service:type_name -> api.Service
20, // 16: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp 14, // 16: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer
21, // 17: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty 20, // 17: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp
3, // 18: api.Machine.InitCluster:input_type -> api.InitClusterRequest 21, // 18: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
5, // 19: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest 3, // 19: api.Machine.InitCluster:input_type -> api.InitClusterRequest
21, // 20: api.Machine.Token:input_type -> google.protobuf.Empty 5, // 20: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
21, // 21: api.Machine.Inspect:input_type -> google.protobuf.Empty 21, // 21: api.Machine.Token:input_type -> google.protobuf.Empty
21, // 22: api.Machine.InspectMachine:input_type -> google.protobuf.Empty 21, // 22: api.Machine.Inspect:input_type -> google.protobuf.Empty
21, // 23: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty 21, // 23: api.Machine.InspectMachine:input_type -> google.protobuf.Empty
9, // 24: api.Machine.Reset:input_type -> api.ResetRequest 21, // 24: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty
11, // 25: api.Machine.InspectService:input_type -> api.InspectServiceRequest 9, // 25: api.Machine.Reset:input_type -> api.ResetRequest
2, // 26: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse 11, // 26: api.Machine.InspectService:input_type -> api.InspectServiceRequest
4, // 27: api.Machine.InitCluster:output_type -> api.InitClusterResponse 2, // 27: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
21, // 28: api.Machine.JoinCluster:output_type -> google.protobuf.Empty 4, // 28: api.Machine.InitCluster:output_type -> api.InitClusterResponse
8, // 29: api.Machine.Token:output_type -> api.TokenResponse 21, // 29: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
0, // 30: api.Machine.Inspect:output_type -> api.MachineInfo 8, // 30: api.Machine.Token:output_type -> api.TokenResponse
6, // 31: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse 0, // 31: api.Machine.Inspect:output_type -> api.MachineInfo
13, // 32: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse 6, // 32: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse
21, // 33: api.Machine.Reset:output_type -> google.protobuf.Empty 13, // 33: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse
12, // 34: api.Machine.InspectService:output_type -> api.InspectServiceResponse 21, // 34: api.Machine.Reset:output_type -> google.protobuf.Empty
26, // [26:35] is the sub-list for method output_type 12, // 35: api.Machine.InspectService:output_type -> api.InspectServiceResponse
17, // [17:26] is the sub-list for method input_type 27, // [27:36] is the sub-list for method output_type
17, // [17:17] is the sub-list for extension type_name 18, // [18:27] is the sub-list for method input_type
17, // [17:17] is the sub-list for extension extendee 18, // [18:18] is the sub-list for extension type_name
0, // [0:17] is the sub-list for field type_name 18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_machine_proto_init() } func init() { file_internal_machine_api_pb_machine_proto_init() }
+3
View File
@@ -55,6 +55,9 @@ message InitClusterRequest {
IP public_ip = 3; IP public_ip = 3;
bool public_ip_auto = 4; bool public_ip_auto = 4;
} }
// Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones.
repeated IPPort wireguard_endpoints = 5;
} }
message InitClusterResponse { message InitClusterResponse {
+49 -3
View File
@@ -376,8 +376,6 @@ func (cc *clusterController) waitStoreSync(ctx context.Context) {
} }
if version >= minVersion { if version >= minVersion {
slog.Info("Cluster store completed the initial sync.", "version", version, "min_version", minVersion)
// Clear MinStoreDBVersion so next restart doesn't wait for sync. // Clear MinStoreDBVersion so next restart doesn't wait for sync.
cc.state.mu.Lock() cc.state.mu.Lock()
cc.state.MinStoreDBVersion = 0 cc.state.MinStoreDBVersion = 0
@@ -386,6 +384,25 @@ func (cc *clusterController) waitStoreSync(ctx context.Context) {
} }
cc.state.mu.Unlock() cc.state.mu.Unlock()
// Wait for all known missing changes to be synced before returning.
// TODO: reevaluate if this is necessary after migrating to the latest Corrosion version:
// https://github.com/psviderski/uncloud/issues/172
// This works on the best effort basis as the missing changes may not be yet known when we start
// checking it after reaching the minimum version.
// Reaching the minimum version doesn't guarantee that the store is actually synced to
// the state we observed on the source node. db_version is a machine-local Lamport clock.
// When changes are received from a remote machine, the local db_version is set to
// max(local_db_version, incoming_db_version) + 1 for each applied transaction. This means
// the new machine's db_version can jump well past the source machine's db_version on the very
// first batch of replicated changes, without having received all changes from all machines
// in the cluster.
cc.waitKnownMissingChanges(ctx)
if ver, verErr := cc.store.DBVersion(ctx); verErr == nil {
version = ver
}
slog.Info("Cluster store completed the initial sync.", "version", version, "min_version", minVersion)
return return
} }
@@ -399,6 +416,34 @@ func (cc *clusterController) waitStoreSync(ctx context.Context) {
} }
} }
// waitKnownMissingChanges polls the store until all known missing changes have been synced.
func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
changes, err := cc.store.KnownMissingChanges(ctx)
if err != nil {
slog.Error("Failed to get known missing changes from the cluster store, skipping check.",
"err", err)
return
}
if len(changes) == 0 {
slog.Debug("All known missing changes have been synced to the cluster store.")
return
}
slog.Debug("Waiting for known missing changes to be synced to the cluster store.", "remaining",
len(changes))
}
}
}
// syncDockerContainers watches local Docker containers and syncs them to the cluster store. // syncDockerContainers watches local Docker containers and syncs them to the cluster store.
// TODO: move this to the Docker controller. // TODO: move this to the Docker controller.
func (cc *clusterController) syncDockerContainers(ctx context.Context) error { func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
@@ -458,7 +503,8 @@ func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
// The machine store may be empty when a machine first joins the cluster, before store synchronization // The machine store may be empty when a machine first joins the cluster, before store synchronization
// completes. Skip configuration now and apply it when the store changes are received. // completes. Skip configuration now and apply it when the store changes are received.
// TODO: remove this check after releasing 0.17.0 and assuming cluster machines wait for store sync on join. // TODO: remove this check after ensuring the store is actually synced to the latest known state at this point.
// See TODO in waitStoreSync.
if len(machines) > 0 { if len(machines) > 0 {
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines)) slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
if err = cc.configurePeers(machines); err != nil { if err = cc.configurePeers(machines); err != nil {
+1 -1
View File
@@ -259,7 +259,7 @@ func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineReques
updatedMachine.PublicIp = req.PublicIp updatedMachine.PublicIp = req.PublicIp
} }
} }
if req.Endpoints != nil { if len(req.Endpoints) > 0 {
updatedMachine.Network.Endpoints = req.Endpoints updatedMachine.Network.Endpoints = req.Endpoints
} }
+22 -13
View File
@@ -701,20 +701,29 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err) return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
} }
} }
// Use the public and all routable IPs as endpoints.
ips, err := network.ListRoutableIPs() // Use explicitly provided WireGuard endpoints, or use the routable IPs on the machine and its public IP
if err != nil { // if not provided. The IPs are auto-detected.
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err) var endpoints []*pb.IPPort
}
publicIP, pubIPErr := network.GetPublicIP() publicIP, pubIPErr := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services.
if pubIPErr == nil && !slices.Contains(ips, publicIP) { if len(req.WireguardEndpoints) > 0 {
ips = append(ips, publicIP) endpoints = req.WireguardEndpoints
} } else {
endpoints := make([]*pb.IPPort, len(ips)) ips, err := network.ListRoutableIPs()
for i, addr := range ips { if err != nil {
addrPort := netip.AddrPortFrom(addr, network.WireGuardPort) return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
endpoints[i] = pb.NewIPPort(addrPort) }
// Ignore the error if failed to get the public IP using API services.
if pubIPErr == nil && !slices.Contains(ips, publicIP) {
ips = append(ips, publicIP)
}
endpoints = make([]*pb.IPPort, len(ips))
for i, addr := range ips {
addrPort := netip.AddrPortFrom(addr, network.WireGuardPort)
endpoints[i] = pb.NewIPPort(addrPort)
}
} }
// Register the new machine in the cluster to populate the state and get its ID and subnet. // Register the new machine in the cluster to populate the state and get its ID and subnet.
+6
View File
@@ -31,6 +31,12 @@ func ListRoutableIPs() ([]netip.Addr, error) {
// Skip Docker bridge interfaces. // Skip Docker bridge interfaces.
continue continue
} }
if iface.Name == "tailscale0" {
// Skip Tailscale interface as we don't want double-tunneling. Also, MTU=1280 on tailscale0 is too small
// for tunneling IPv6 used for uncloud management traffic.
// See Discord thread: https://discord.com/channels/1371726032104587335/1477823130670993408
continue
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
// Skip interfaces: // Skip interfaces:
+30
View File
@@ -3,6 +3,7 @@ package store
import ( import (
"context" "context"
_ "embed" _ "embed"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -75,6 +76,35 @@ func (s *Store) DBVersion(ctx context.Context) (int64, error) {
return version, nil return version, nil
} }
type MissingChange struct {
ActorID string
StartVersion int64
EndVersion int64
}
// KnownMissingChanges returns a list of currently known missing changes in the Corrosion database.
func (s *Store) KnownMissingChanges(ctx context.Context) ([]MissingChange, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT actor_id, start, end FROM __corro_bookkeeping_gaps")
if err != nil {
return nil, fmt.Errorf("query missing changes: %w", err)
}
defer rows.Close()
var changes []MissingChange
for rows.Next() {
var c MissingChange
var actorBytes []byte
if err = rows.Scan(&actorBytes, &c.StartVersion, &c.EndVersion); err != nil {
return nil, fmt.Errorf("scan missing change: %w", err)
}
c.ActorID = hex.EncodeToString(actorBytes)
changes = append(changes, c)
}
return changes, nil
}
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error { func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
mJSON, err := protojson.Marshal(m) mJSON, err := protojson.Marshal(m)
if err != nil { if err != nil {
+5 -2
View File
@@ -55,6 +55,8 @@ type ServiceSpec struct {
// Caddy is the optional Caddy reverse proxy configuration for the service. // Caddy is the optional Caddy reverse proxy configuration for the service.
// Caddy and Ports cannot be specified simultaneously. // Caddy and Ports cannot be specified simultaneously.
Caddy *CaddySpec `json:",omitempty"` Caddy *CaddySpec `json:",omitempty"`
// Configs is list of configuration objects that can be mounted into the container.
Configs []ConfigSpec
// Container defines the desired state of each container in the service. // Container defines the desired state of each container in the service.
Container ContainerSpec Container ContainerSpec
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty. // Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
@@ -67,12 +69,13 @@ type ServiceSpec struct {
Ports []PortSpec Ports []PortSpec
// Replicas is the number of containers to run for the service. Only valid for a replicated service. // Replicas is the number of containers to run for the service. Only valid for a replicated service.
Replicas uint `json:",omitempty"` Replicas uint `json:",omitempty"`
// StopGracePeriod is how long to wait after SIGTERM before sending SIGKILL when stopping a container.
// Default is 10 seconds if not specified.
StopGracePeriod *time.Duration `json:",omitempty"`
// UpdateConfig configures how the service is updated during a deployment. // UpdateConfig configures how the service is updated during a deployment.
UpdateConfig UpdateConfig `json:",omitempty"` UpdateConfig UpdateConfig `json:",omitempty"`
// Volumes is list of data volumes that can be mounted into the container. // Volumes is list of data volumes that can be mounted into the container.
Volumes []VolumeSpec Volumes []VolumeSpec
// Configs is list of configuration objects that can be mounted into the container.
Configs []ConfigSpec
} }
// CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined. // CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined.
+5
View File
@@ -85,6 +85,11 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
} }
} }
if service.StopGracePeriod != nil {
d := time.Duration(*service.StopGracePeriod)
spec.StopGracePeriod = &d
}
if service.Scale != nil { if service.Scale != nil {
spec.Replicas = uint(*service.Scale) spec.Replicas = uint(*service.Scale)
} }
+2 -1
View File
@@ -205,7 +205,8 @@ func TestServiceSpecFromCompose(t *testing.T) {
Placement: api.Placement{ Placement: api.Placement{
Machines: []string{"machine-1", "machine-2"}, Machines: []string{"machine-1", "machine-2"},
}, },
Replicas: 3, Replicas: 3,
StopGracePeriod: api.AsPtr(30 * time.Second),
UpdateConfig: api.UpdateConfig{ UpdateConfig: api.UpdateConfig{
Order: api.UpdateOrderStopFirst, Order: api.UpdateOrderStopFirst,
MonitorPeriod: &api.DefaultHealthMonitorPeriod, MonitorPeriod: &api.DefaultHealthMonitorPeriod,
+1
View File
@@ -37,6 +37,7 @@ services:
privileged: true privileged: true
pull_policy: always pull_policy: always
scale: 3 scale: 3
stop_grace_period: 30s
sysctls: sysctls:
- net.ipv4.ip_forward=1 - net.ipv4.ip_forward=1
ulimits: ulimits:
+28 -12
View File
@@ -3,6 +3,7 @@ package operation
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -53,13 +54,14 @@ func (o *RunContainerOperation) String() string {
// StopContainerOperation stops a container on a specific machine. // StopContainerOperation stops a container on a specific machine.
type StopContainerOperation struct { type StopContainerOperation struct {
ServiceID string ServiceID string
ContainerID string ContainerID string
MachineID string MachineID string
StopGracePeriod *time.Duration
} }
func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error { func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error {
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil { if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, stopOptions(o.StopGracePeriod)); err != nil {
return fmt.Errorf("stop container: %w", err) return fmt.Errorf("stop container: %w", err)
} }
return nil return nil
@@ -78,15 +80,18 @@ func (o *StopContainerOperation) String() string {
// RemoveContainerOperation stops and removes a container from a specific machine. // RemoveContainerOperation stops and removes a container from a specific machine.
type RemoveContainerOperation struct { type RemoveContainerOperation struct {
MachineID string MachineID string
Container api.ServiceContainer Container api.ServiceContainer
StopGracePeriod *time.Duration
} }
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) error { func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) error {
if err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.StopOptions{}); err != nil { err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, stopOptions(o.StopGracePeriod))
if err != nil {
return fmt.Errorf("stop container: %w", err) return fmt.Errorf("stop container: %w", err)
} }
if err := cli.RemoveContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.RemoveOptions{
if err = cli.RemoveContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.RemoveOptions{
// Remove anonymous volumes created by the container. // Remove anonymous volumes created by the container.
RemoveVolumes: true, RemoveVolumes: true,
}); err != nil { }); err != nil {
@@ -119,6 +124,7 @@ type ReplaceContainerOperation struct {
Order string Order string
// SkipHealthMonitor skips the monitoring period and health checks after starting a new container. // SkipHealthMonitor skips the monitoring period and health checks after starting a new container.
SkipHealthMonitor bool SkipHealthMonitor bool
StopGracePeriod *time.Duration
} }
func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) error { func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) error {
@@ -133,7 +139,8 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err
} }
wasRunning = ctr.Container.State.Running wasRunning = ctr.Container.State.Running
if wasRunning { if wasRunning {
if err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, container.StopOptions{}); err != nil { err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, stopOptions(o.StopGracePeriod))
if err != nil {
return fmt.Errorf("stop old container: %w", err) return fmt.Errorf("stop old container: %w", err)
} }
} }
@@ -156,7 +163,7 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err
// Use context without progress to not overwrite the container Unhealthy status with Stopped. // Use context without progress to not overwrite the container Unhealthy status with Stopped.
ctxWithoutProgress := progress.WithContextWriter(ctx, nil) ctxWithoutProgress := progress.WithContextWriter(ctx, nil)
_ = cli.StopContainer(ctxWithoutProgress, o.ServiceID, resp.ID, container.StopOptions{}) _ = cli.StopContainer(ctxWithoutProgress, o.ServiceID, resp.ID, stopOptions(o.StopGracePeriod))
newCtr := fmt.Sprintf("%s/%s", o.Spec.Name, stringid.TruncateID(resp.ID)) newCtr := fmt.Sprintf("%s/%s", o.Spec.Name, stringid.TruncateID(resp.ID))
healthErr := fmt.Errorf( healthErr := fmt.Errorf(
@@ -186,12 +193,12 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err
// There still might be a brief downtime (for a 1 replica service) when Caddy doesn't know about // There still might be a brief downtime (for a 1 replica service) when Caddy doesn't know about
// the new container but we're stopping the old container. We should somehow ensure Caddy is updated // the new container but we're stopping the old container. We should somehow ensure Caddy is updated
// with the new container before we stop the old one to avoid this downtime. // with the new container before we stop the old one to avoid this downtime.
if err := cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, container.StopOptions{}); err != nil { if err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, stopOptions(o.StopGracePeriod)); err != nil {
return fmt.Errorf("stop old container: %w", err) return fmt.Errorf("stop old container: %w", err)
} }
} }
if err := cli.RemoveContainer(ctx, o.ServiceID, o.OldContainer.ID, container.RemoveOptions{ if err = cli.RemoveContainer(ctx, o.ServiceID, o.OldContainer.ID, container.RemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
}); err != nil { }); err != nil {
return fmt.Errorf("remove old container: %w", err) return fmt.Errorf("remove old container: %w", err)
@@ -209,3 +216,12 @@ func (o *ReplaceContainerOperation) String() string {
return fmt.Sprintf("ReplaceContainerOperation[machine_id=%s service_id=%s old_container_id=%s order=%s]", return fmt.Sprintf("ReplaceContainerOperation[machine_id=%s service_id=%s old_container_id=%s order=%s]",
o.MachineID, o.ServiceID, o.OldContainer.ID, o.Order) o.MachineID, o.ServiceID, o.OldContainer.ID, o.Order)
} }
// stopOptions converts a stop grace period duration to Docker container stop options.
func stopOptions(gracePeriod *time.Duration) container.StopOptions {
if gracePeriod == nil {
return container.StopOptions{}
}
t := int(gracePeriod.Seconds())
return container.StopOptions{Timeout: &t}
}
+21 -13
View File
@@ -175,6 +175,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
OldContainer: ctr, OldContainer: ctr,
Order: order, Order: order,
SkipHealthMonitor: s.SkipHealthMonitor, SkipHealthMonitor: s.SkipHealthMonitor,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
@@ -182,8 +183,9 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
for mid, containers := range containersOnMachine { for mid, containers := range containersOnMachine {
for _, c := range containers { for _, c := range containers {
plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{
MachineID: mid, MachineID: mid,
Container: c, Container: c,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
} }
@@ -234,8 +236,9 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Pl
for _, containers := range containersOnMachine { for _, containers := range containersOnMachine {
for _, c := range containers { for _, c := range containers {
plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{
MachineID: c.MachineID, MachineID: c.MachineID,
Container: c.Container, Container: c.Container,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
} }
@@ -286,8 +289,9 @@ func reconcileGlobalContainer(
continue continue
} }
ops = append(ops, &operation.RemoveContainerOperation{ ops = append(ops, &operation.RemoveContainerOperation{
MachineID: old.MachineID, MachineID: old.MachineID,
Container: old.Container, Container: old.Container,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
break break
@@ -319,9 +323,10 @@ func reconcileGlobalContainer(
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports) conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
if err != nil || len(conflictingPorts) > 0 { if err != nil || len(conflictingPorts) > 0 {
ops = append(ops, &operation.StopContainerOperation{ ops = append(ops, &operation.StopContainerOperation{
ServiceID: serviceID, ServiceID: serviceID,
ContainerID: c.Container.ID, ContainerID: c.Container.ID,
MachineID: machineID, MachineID: machineID,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
} }
@@ -335,6 +340,7 @@ func reconcileGlobalContainer(
OldContainer: containerToReplace.Container, OldContainer: containerToReplace.Container,
Order: order, Order: order,
SkipHealthMonitor: skipHealthCheck, SkipHealthMonitor: skipHealthCheck,
StopGracePeriod: spec.StopGracePeriod,
}) })
// Remove any other containers (there shouldn't be any in normal operation). // Remove any other containers (there shouldn't be any in normal operation).
@@ -343,8 +349,9 @@ func reconcileGlobalContainer(
continue continue
} }
ops = append(ops, &operation.RemoveContainerOperation{ ops = append(ops, &operation.RemoveContainerOperation{
MachineID: c.MachineID, MachineID: c.MachineID,
Container: c.Container, Container: c.Container,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
} else { } else {
@@ -357,8 +364,9 @@ func reconcileGlobalContainer(
}) })
for _, c := range containers { for _, c := range containers {
ops = append(ops, &operation.RemoveContainerOperation{ ops = append(ops, &operation.RemoveContainerOperation{
MachineID: c.MachineID, MachineID: c.MachineID,
Container: c.Container, Container: c.Container,
StopGracePeriod: spec.StopGracePeriod,
}) })
} }
} }
+54 -13
View File
@@ -75,24 +75,17 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) (
close(reachableMachines) close(reachableMachines)
}() }()
var ingressIPs []string var machines []*pb.MachineInfo
for m := range reachableMachines { for m := range reachableMachines {
ip, _ := m.PublicIp.ToAddr() machines = append(machines, m)
ingressIPs = append(ingressIPs, ip.String())
} }
if len(ingressIPs) == 0 { if len(machines) == 0 {
return nil, ErrNoReachableMachines return nil, ErrNoReachableMachines
} }
req := &pb.CreateDomainRecordsRequest{ req, err := getCreateDomainRecordsRequest(machines)
Records: []*pb.DNSRecord{ if err != nil {
{ return nil, fmt.Errorf("create CreateDomainRecordsRequest: %w", err)
Name: "*",
Type: pb.DNSRecord_A,
Values: ingressIPs,
},
// TODO: Add AAAA record with routable IPv6 addresses of machines running Caddy containers.
},
} }
resp, err := cli.CreateDomainRecords(ctx, req) resp, err := cli.CreateDomainRecords(ctx, req)
if err != nil { if err != nil {
@@ -102,6 +95,54 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) (
return resp.Records, nil return resp.Records, nil
} }
func getCreateDomainRecordsRequest(machines []*pb.MachineInfo) (*pb.CreateDomainRecordsRequest, error) {
if len(machines) == 0 {
return nil, fmt.Errorf("at least one machine must be provided")
}
var ipv4IngressIPs []string
var ipv6IngressIPs []string
var errs error
for _, m := range machines {
ip, _ := m.PublicIp.ToAddr()
if ip.Is4() {
ipv4IngressIPs = append(ipv4IngressIPs, ip.String())
} else if ip.Is6() {
ipv6IngressIPs = append(ipv6IngressIPs, ip.String())
} else {
// This is just a save guard, in case some special case is ever missed.
errs = errors.Join(errs, fmt.Errorf("machine with name %s (and ID: %s) has the public IP address '%s' which is neither IPv4 nor IPv6", m.Name, m.Id, ip.String()))
}
}
if errs != nil {
return nil, errs
}
records := make([]*pb.DNSRecord, 0, 2)
if len(ipv4IngressIPs) > 0 {
records = append(records,
&pb.DNSRecord{
Name: "*",
Type: pb.DNSRecord_A,
Values: ipv4IngressIPs,
},
)
}
if len(ipv6IngressIPs) > 0 {
records = append(records,
&pb.DNSRecord{
Name: "*",
Type: pb.DNSRecord_AAAA,
Values: ipv6IngressIPs,
},
)
}
return &pb.CreateDomainRecordsRequest{
Records: records,
}, nil
}
// verifyCaddyReachable verifies that the Caddy service is reachable on the machine by its public IP. // verifyCaddyReachable verifies that the Caddy service is reachable on the machine by its public IP.
func verifyCaddyReachable(ctx context.Context, m *pb.MachineInfo) error { func verifyCaddyReachable(ctx context.Context, m *pb.MachineInfo) error {
publicIP, _ := m.PublicIp.ToAddr() publicIP, _ := m.PublicIp.ToAddr()
+74
View File
@@ -1,10 +1,13 @@
package client package client
import ( import (
"fmt"
"net/netip" "net/netip"
"testing" "testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGetVerifyURL(t *testing.T) { func TestGetVerifyURL(t *testing.T) {
@@ -28,3 +31,74 @@ func TestGetVerifyURL(t *testing.T) {
}) })
} }
} }
func TestGetCreateDomainRecordsRequest(t *testing.T) {
tests := map[string]struct {
given []*pb.MachineInfo
wantReq *pb.CreateDomainRecordsRequest
wantErr error
}{
"single IPv4 machine": {
given: []*pb.MachineInfo{
{Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))},
},
wantReq: &pb.CreateDomainRecordsRequest{
Records: []*pb.DNSRecord{
{Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4"}},
},
},
},
"single IPv6 machine": {
given: []*pb.MachineInfo{
{Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::1"))},
},
wantReq: &pb.CreateDomainRecordsRequest{
Records: []*pb.DNSRecord{
{Name: "*", Type: pb.DNSRecord_AAAA, Values: []string{"2001:db8::1"}},
},
},
},
"multiple IPv4 machines": {
given: []*pb.MachineInfo{
{Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))},
{Id: "m2", Name: "machine-2", PublicIp: pb.NewIP(netip.MustParseAddr("5.6.7.8"))},
},
wantReq: &pb.CreateDomainRecordsRequest{
Records: []*pb.DNSRecord{
{Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4", "5.6.7.8"}},
},
},
},
"mixed IPv4 and IPv6 machines": {
given: []*pb.MachineInfo{
{Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))},
{Id: "m2", Name: "machine-2", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::1"))},
{Id: "m3", Name: "machine-3", PublicIp: pb.NewIP(netip.MustParseAddr("10.0.0.1"))},
{Id: "m4", Name: "machine-4", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::2"))},
},
wantReq: &pb.CreateDomainRecordsRequest{
Records: []*pb.DNSRecord{
{Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4", "10.0.0.1"}},
{Name: "*", Type: pb.DNSRecord_AAAA, Values: []string{"2001:db8::1", "2001:db8::2"}},
},
},
},
"empty machine list": {
given: nil,
wantErr: fmt.Errorf("at least one machine must be provided"),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
gotReq, gotErr := getCreateDomainRecordsRequest(tt.given)
if tt.wantErr == nil {
require.NoError(t, gotErr)
assert.Equal(t, tt.wantReq, gotReq)
} else {
assert.Nil(t, gotReq)
assert.Equal(t, tt.wantErr.Error(), gotErr.Error())
}
})
}
}
+1 -1
View File
@@ -120,7 +120,7 @@ func (cli *Client) WaitMachineReady(ctx context.Context, timeout time.Duration)
return backoff.Retry(inspect, boff) return backoff.Retry(inspect, boff)
} }
// WaitClusterReady waits for the connected machine to be ready to server cluster requests. // WaitClusterReady waits for the connected machine to be ready to serve cluster requests.
func (cli *Client) WaitClusterReady(ctx context.Context, timeout time.Duration) error { func (cli *Client) WaitClusterReady(ctx context.Context, timeout time.Duration) error {
// Backoff is not really needed here as the default service config for the gRPC client is already // Backoff is not really needed here as the default service config for the gRPC client is already
// doing retries with backoff for Unavailable errors. However, it's still convenient to use backoff // doing retries with backoff for Unavailable errors. However, it's still convenient to use backoff
+1 -1
View File
@@ -55,7 +55,7 @@ func createTestCluster(
}) })
if waitReady { if waitReady {
require.NoError(t, p.WaitClusterReady(ctx, c, 30*time.Second)) require.NoError(t, p.WaitClusterReady(ctx, c, 60*time.Second))
} }
return c, p return c, p
-215
View File
@@ -1,215 +0,0 @@
package e2e
import (
"context"
"regexp"
"strings"
"testing"
"time"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestInternalDNS tests the internal DNS functionality including the new machine-specific service lookups
func TestInternalDNS(t *testing.T) {
t.Parallel()
clusterName := "ucind-test.dns"
ctx := context.Background()
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
cli, err := c.Machines[0].Connect(ctx)
require.NoError(t, err)
// Create a test service with multiple replicas across machines
serviceName := "test-dns-service"
t.Cleanup(func() {
err := cli.RemoveService(ctx, serviceName)
if err != nil && !strings.Contains(err.Error(), "not found") {
require.NoError(t, err)
}
})
// Deploy a service across all machines in global mode using pause container
spec := api.ServiceSpec{
Name: serviceName,
Mode: api.ServiceModeGlobal,
Container: api.ContainerSpec{
Image: "portainer/pause:latest",
},
}
deployment := cli.NewDeployment(spec, nil)
_, err = deployment.Run(ctx)
require.NoError(t, err)
// Wait for the service to be deployed
var svc api.Service
require.Eventually(t, func() bool {
svc, err = cli.InspectService(ctx, serviceName)
if err != nil {
return false
}
// Should have 3 containers (one per machine) and all should be running
if len(svc.Containers) != 3 {
return false
}
for _, ctr := range svc.Containers {
if ctr.Container.State.Status != "running" {
return false
}
}
return true
}, 30*time.Second, 1*time.Second, "Service should be deployed and running on all machines")
// Deploy a single "network-multitool" service to be used for all DNS queries
queryServiceName := "dns-query-service"
t.Cleanup(func() {
err := cli.RemoveService(ctx, queryServiceName)
if err != nil && !strings.Contains(err.Error(), "not found") {
require.NoError(t, err)
}
})
querySvcSpec := api.ServiceSpec{
Name: queryServiceName,
Mode: api.ServiceModeReplicated,
Replicas: 1,
Placement: api.Placement{
Machines: []string{c.Machines[0].Name},
},
Container: api.ContainerSpec{
Image: "wbitt/network-multitool",
Command: []string{"sleep", "infinity"},
},
}
_, err = cli.RunService(ctx, querySvcSpec)
require.NoError(t, err)
// Wait for the query service to be deployed
var querySvc api.Service
require.Eventually(t, func() bool {
querySvc, err = cli.InspectService(ctx, queryServiceName)
if err != nil {
return false
}
// Should have 1 container and it should be running
if len(querySvc.Containers) != 1 {
return false
}
return querySvc.Containers[0].Container.State.Status == "running"
}, 30*time.Second, 1*time.Second, "Query service should be deployed and running")
queryContainer := querySvc.Containers[0]
// Run nslookup for given query
runNslookup := func(t *testing.T, dnsQuery string) string {
dnsOutput, err := execInContainerAndReadOutput(
t, ctx, cli, queryServiceName, queryContainer.Container.ID,
[]string{"nslookup", dnsQuery},
)
require.NoError(t, err)
return dnsOutput
}
// Helper function to verify DNS output doesn't contain errors
assertNoDNSErrors := func(t *testing.T, dnsOutput string) {
assert.NotContains(t, dnsOutput, "can't resolve", "DNS query should not contain resolution errors")
assert.NotContains(t, dnsOutput, "Name or service not known",
"DNS query should not contain unknown service errors")
}
t.Run("service name resolves to all container IPs", func(t *testing.T) {
dnsOutput := runNslookup(t, serviceName+".internal")
t.Logf("DNS query output:\n%s", dnsOutput)
// Verify that all service container IPs are in the DNS response
for _, ctr := range svc.Containers {
containerIP := ctr.Container.UncloudNetworkIP().String()
assert.Contains(t, dnsOutput, containerIP,
"Service DNS should resolve to container IP %s", containerIP)
}
assertNoDNSErrors(t, dnsOutput)
})
t.Run("machine-specific service DNS lookups", func(t *testing.T) {
// Test the new <machine-id>.m.<service-name>.internal DNS feature
for _, targetContainer := range svc.Containers {
targetMachineID := targetContainer.MachineID
targetContainerIP := targetContainer.Container.UncloudNetworkIP().String()
// Construct the machine-specific DNS name
machineSpecificDNS := targetMachineID + ".m." + serviceName + ".internal"
dnsOutput := runNslookup(t, machineSpecificDNS)
t.Logf("Machine-specific DNS query output for %s:\n%s", machineSpecificDNS, dnsOutput)
// Verify that the specific container IP is returned
assert.Contains(t, dnsOutput, targetContainerIP,
"Machine-specific DNS %s should resolve to container IP %s",
machineSpecificDNS, targetContainerIP)
// Verify that other container IPs are not returned (machine-specific should return only one IP)
for _, ctr := range svc.Containers {
if ctr.MachineID != targetMachineID {
otherContainerIP := ctr.Container.UncloudNetworkIP().String()
assert.NotContains(t, dnsOutput, otherContainerIP,
"Machine-specific DNS %s should not resolve to other container IP %s",
machineSpecificDNS, otherContainerIP)
}
}
assertNoDNSErrors(t, dnsOutput)
}
})
t.Run("service ID DNS lookup", func(t *testing.T) {
// Test that service ID also resolves (existing functionality)
dnsOutput := runNslookup(t, svc.ID+".internal")
t.Logf("Service ID DNS query output:\n%s", dnsOutput)
// Verify that all service container IPs are in the DNS response
for _, ctr := range svc.Containers {
containerIP := ctr.Container.UncloudNetworkIP().String()
assert.Contains(t, dnsOutput, containerIP,
"Service ID DNS should resolve to container IP %s", containerIP)
}
assertNoDNSErrors(t, dnsOutput)
})
t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) {
// Find the service container on the same machine as the query container.
var localIP string
for _, ctr := range svc.Containers {
if ctr.MachineID == queryContainer.MachineID {
localIP = ctr.Container.UncloudNetworkIP().String()
break
}
}
require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryContainer.MachineID)
// We will extract the first IP address from the DNS output using a regex.
// Pattern matches "Name: nearest.test-dns-service.internal" followed by "Address: X.X.X.X".
re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`)
// Test the "nearest" mode which should sort local subnet IPs first.
// The default behavior randomizes the order, so run it a few times
// to reduce the chance we're just getting lucky with the order.
for range 5 {
dnsOutput := runNslookup(t, "nearest."+serviceName+".internal")
t.Logf("Nearest mode DNS query output:\n%s", dnsOutput)
matches := re.FindStringSubmatch(dnsOutput)
require.Len(t, matches, 2, "Should find Name's Address in DNS output")
firstIP := matches[1]
assert.Equal(t, localIP, firstIP,
"Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)",
queryContainer.MachineID, localIP, firstIP)
assertNoDNSErrors(t, dnsOutput)
}
})
}
+61 -75
View File
@@ -12,10 +12,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMachineRename(t *testing.T) { func TestMachineOperations(t *testing.T) {
t.Parallel() t.Parallel()
name := "ucind-test.machine-rename" name := "ucind-test.machine-ops"
ctx := context.Background() ctx := context.Background()
c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true) c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true)
@@ -23,59 +23,61 @@ func TestMachineRename(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer cli.Close() defer cli.Close()
// RenameMachine subtests.
t.Run("rename machine by name", func(t *testing.T) { t.Run("rename machine by name", func(t *testing.T) {
// Get initial machine state // Get initial machine state.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Select the second machine to rename // Select the second machine to rename.
originalMachine := machines[1] originalMachine := machines[1]
originalName := originalMachine.Machine.Name originalName := originalMachine.Machine.Name
newName := "renamed-machine-1" newName := "renamed-machine-1"
// Rename the machine // Rename the machine.
updatedMachine, err := cli.RenameMachine(ctx, originalName, newName) updatedMachine, err := cli.RenameMachine(ctx, originalName, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, originalMachine.Machine.Id, updatedMachine.Id) assert.Equal(t, originalMachine.Machine.Id, updatedMachine.Id)
// Verify the machine list reflects the change // Verify the machine list reflects the change.
machines, err = cli.ListMachines(ctx, nil) machines, err = cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Find the renamed machine // Find the renamed machine.
var found bool var found bool
for _, m := range machines { for _, m := range machines {
if m.Machine.Id == originalMachine.Machine.Id { if m.Machine.Id == originalMachine.Machine.Id {
assert.Equal(t, newName, m.Machine.Name) assert.Equal(t, newName, m.Machine.Name)
found = true found = true
} else { } else {
// Ensure other machines are unaffected // Ensure other machines are unaffected.
assert.NotEqual(t, newName, m.Machine.Name) assert.NotEqual(t, newName, m.Machine.Name)
} }
} }
assert.True(t, found, "Renamed machine should be in the list") assert.True(t, found, "Renamed machine should be in the list")
// Verify we can inspect the machine by its new name // Verify we can inspect the machine by its new name.
inspectedMachine, err := cli.InspectMachine(ctx, newName) inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, originalMachine.Machine.Id, inspectedMachine.Machine.Id) assert.Equal(t, originalMachine.Machine.Id, inspectedMachine.Machine.Id)
// Verify the old name no longer works // Verify the old name no longer works.
_, err = cli.InspectMachine(ctx, originalName) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename machine by ID", func(t *testing.T) { t.Run("rename machine by ID", func(t *testing.T) {
// Get the third machine // Get the third machine.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Find a machine that hasn't been renamed yet // Find a machine that hasn't been renamed yet.
var targetMachine *pb.MachineMember var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" { if m.Machine.Name != "renamed-machine-1" {
@@ -89,67 +91,67 @@ func TestMachineRename(t *testing.T) {
machineID := targetMachine.Machine.Id machineID := targetMachine.Machine.Id
newName := "renamed-machine-2" newName := "renamed-machine-2"
// Rename using ID instead of name // Rename using ID instead of name.
updatedMachine, err := cli.RenameMachine(ctx, machineID, newName) updatedMachine, err := cli.RenameMachine(ctx, machineID, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, machineID, updatedMachine.Id) assert.Equal(t, machineID, updatedMachine.Id)
// Verify the rename was successful // Verify the rename was successful.
inspectedMachine, err := cli.InspectMachine(ctx, newName) inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, machineID, inspectedMachine.Machine.Id) assert.Equal(t, machineID, inspectedMachine.Machine.Id)
// Verify the old name no longer works // Verify the old name no longer works.
_, err = cli.InspectMachine(ctx, originalName) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename non-existent machine", func(t *testing.T) { t.Run("rename non-existent machine", func(t *testing.T) {
// Try to rename a machine that doesn't exist // Try to rename a machine that doesn't exist.
_, err := cli.RenameMachine(ctx, "non-existent-machine", "new-name") _, err := cli.RenameMachine(ctx, "non-existent-machine", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
// Try with a non-existent ID // Try with a non-existent ID.
_, err = cli.RenameMachine(ctx, "non-existent-id-12345", "new-name") _, err = cli.RenameMachine(ctx, "non-existent-id-12345", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename to existing name", func(t *testing.T) { t.Run("rename to existing name", func(t *testing.T) {
// Get current machines // Get current machines.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Try to rename machine 0 to the name of machine 1 // Try to rename machine 0 to the name of machine 1.
machine0Name := machines[0].Machine.Name machine0Name := machines[0].Machine.Name
machine1Name := machines[1].Machine.Name machine1Name := machines[1].Machine.Name
// This should fail because the name is already taken // This should fail because the name is already taken.
_, err = cli.RenameMachine(ctx, machine0Name, machine1Name) _, err = cli.RenameMachine(ctx, machine0Name, machine1Name)
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("rename with empty name", func(t *testing.T) { t.Run("rename with empty name", func(t *testing.T) {
// Get a machine to rename // Get a machine to rename.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
machineName := machines[0].Machine.Name machineName := machines[0].Machine.Name
// Try to rename with empty string // Try to rename with empty string.
_, err = cli.RenameMachine(ctx, machineName, "") _, err = cli.RenameMachine(ctx, machineName, "")
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("service continuity after rename", func(t *testing.T) { t.Run("service continuity after rename", func(t *testing.T) {
// Deploy a service on a specific machine // Deploy a service on a specific machine.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
// Find a machine that hasn't been renamed to test with // Find a machine that hasn't been renamed to test with.
var targetMachine *pb.MachineMember var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" && m.Machine.Name != "renamed-machine-2" { if m.Machine.Name != "renamed-machine-1" && m.Machine.Name != "renamed-machine-2" {
@@ -162,7 +164,7 @@ func TestMachineRename(t *testing.T) {
originalMachineName := targetMachine.Machine.Name originalMachineName := targetMachine.Machine.Name
serviceName := "test-service-rename-continuity" serviceName := "test-service-rename-continuity"
// Create a service on the specific machine // Create a service on the specific machine.
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Name: serviceName, Name: serviceName,
Mode: api.ServiceModeGlobal, Mode: api.ServiceModeGlobal,
@@ -184,51 +186,41 @@ func TestMachineRename(t *testing.T) {
} }
}) })
// Verify service is running on the machine // Verify service is running on the machine.
svc, err := cli.InspectService(ctx, serviceName) svc, err := cli.InspectService(ctx, serviceName)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, svc.Containers, 1) assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// Rename the machine // Rename the machine.
newMachineName := "renamed-for-service-test" newMachineName := "renamed-for-service-test"
_, err = cli.RenameMachine(ctx, originalMachineName, newMachineName) _, err = cli.RenameMachine(ctx, originalMachineName, newMachineName)
require.NoError(t, err) require.NoError(t, err)
// Verify service is still running on the renamed machine // Verify service is still running on the renamed machine.
svc, err = cli.InspectService(ctx, serviceName) svc, err = cli.InspectService(ctx, serviceName)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, svc.Containers, 1) assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// The service spec's placement still references the old name, // The service spec's placement still references the old name,
// but the service should continue to run on the same machine id // but the service should continue to run on the same machine id.
}) })
}
func TestUpdateMachine(t *testing.T) { // UpdateMachine subtests.
t.Parallel()
name := "ucind-test.machine-update"
ctx := context.Background()
c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true)
cli, err := c.Machines[0].Connect(ctx)
require.NoError(t, err)
defer cli.Close()
t.Run("update machine name", func(t *testing.T) { t.Run("update machine name", func(t *testing.T) {
// Get initial machine state // Get initial machine state.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Select a machine to update // Select a machine to update.
targetMachine := machines[1] targetMachine := machines[1]
originalName := targetMachine.Machine.Name originalName := targetMachine.Machine.Name
newName := "updated-machine-name" newName := "updated-machine-name"
// Update the machine name using UpdateMachine directly // Update the machine name using UpdateMachine directly.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
Name: &newName, Name: &newName,
@@ -238,22 +230,22 @@ func TestUpdateMachine(t *testing.T) {
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id) assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name) assert.Equal(t, newName, inspected.Machine.Name)
// Verify old name no longer works // Verify old name no longer works.
_, err = cli.InspectMachine(ctx, originalName) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("update machine public IP", func(t *testing.T) { t.Run("update machine public IP", func(t *testing.T) {
// Get a machine to update // Get a machine to update.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
// Find a machine that hasn't been renamed // Find a machine that hasn't been renamed.
var targetMachine *pb.MachineMember var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "updated-machine-name" { if m.Machine.Name != "updated-machine-name" {
@@ -263,12 +255,12 @@ func TestUpdateMachine(t *testing.T) {
} }
require.NotNil(t, targetMachine) require.NotNil(t, targetMachine)
// Create a new public IP (must be a valid public IP address) // Create a new public IP (must be a valid public IP address).
newPublicIP := &pb.IP{ newPublicIP := &pb.IP{
Ip: []byte{8, 8, 8, 8}, Ip: []byte{8, 8, 8, 8},
} }
// Update the public IP // Update the public IP.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
PublicIp: newPublicIP, PublicIp: newPublicIP,
@@ -277,31 +269,31 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id) inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip)
}) })
t.Run("remove machine public IP", func(t *testing.T) { t.Run("remove machine public IP", func(t *testing.T) {
// Get machines // Get machines.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.True(t, len(machines) > 0, "Need at least one machine") require.True(t, len(machines) > 0, "Need at least one machine")
// First, set a public IP on a machine // First, set a public IP on a machine.
targetMachine := machines[0] targetMachine := machines[0]
setIPReq := &pb.UpdateMachineRequest{ setIPReq := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
PublicIp: &pb.IP{Ip: []byte{192, 0, 2, 1}}, // TEST-NET-1 address PublicIp: &pb.IP{Ip: []byte{192, 0, 2, 1}}, // TEST-NET-1 address.
} }
updatedMachine, err := cli.UpdateMachine(ctx, setIPReq) updatedMachine, err := cli.UpdateMachine(ctx, setIPReq)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, updatedMachine.PublicIp) require.NotNil(t, updatedMachine.PublicIp)
// Now test removing the public IP // Now test removing the public IP.
// Remove the public IP by setting it to empty // Remove the public IP by setting it to empty.
emptyIP := &pb.IP{} emptyIP := &pb.IP{}
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: updatedMachine.Id, MachineId: updatedMachine.Id,
@@ -311,23 +303,17 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, removedIPMachine.PublicIp) assert.Nil(t, removedIPMachine.PublicIp)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, inspected.Machine.PublicIp) assert.Nil(t, inspected.Machine.PublicIp)
}) })
t.Run("update machine endpoints", func(t *testing.T) { t.Run("update machine endpoints", func(t *testing.T) {
// Get a machine to update // Get a machine to update.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
targetMachine := machines[0]
var targetMachine *pb.MachineMember
for _, m := range machines {
targetMachine = m
break
}
require.NotNil(t, targetMachine)
newEndpoints := []*pb.IPPort{ newEndpoints := []*pb.IPPort{
{ {
@@ -348,13 +334,13 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints)) assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints))
// Verify endpoints were updated // Verify endpoints were updated.
for i, endpoint := range updatedMachine.Network.Endpoints { for i, endpoint := range updatedMachine.Network.Endpoints {
assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip) assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip)
assert.Equal(t, newEndpoints[i].Port, endpoint.Port) assert.Equal(t, newEndpoints[i].Port, endpoint.Port)
} }
// Verify other network fields remain unchanged // Verify other network fields remain unchanged.
assert.Equal(t, targetMachine.Machine.Network.Subnet.Ip.Ip, updatedMachine.Network.Subnet.Ip.Ip) assert.Equal(t, targetMachine.Machine.Network.Subnet.Ip.Ip, updatedMachine.Network.Subnet.Ip.Ip)
assert.Equal(t, targetMachine.Machine.Network.Subnet.Bits, updatedMachine.Network.Subnet.Bits) assert.Equal(t, targetMachine.Machine.Network.Subnet.Bits, updatedMachine.Network.Subnet.Bits)
assert.Equal(t, targetMachine.Machine.Network.ManagementIp.Ip, updatedMachine.Network.ManagementIp.Ip) assert.Equal(t, targetMachine.Machine.Network.ManagementIp.Ip, updatedMachine.Network.ManagementIp.Ip)
@@ -362,7 +348,7 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update multiple fields simultaneously", func(t *testing.T) { t.Run("update multiple fields simultaneously", func(t *testing.T) {
// Get a machine to update // Get a machine to update.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -375,7 +361,7 @@ func TestUpdateMachine(t *testing.T) {
} }
require.NotNil(t, targetMachine) require.NotNil(t, targetMachine)
// Update both name and public IP // Update both name and public IP.
newName := "multi-update-machine" newName := "multi-update-machine"
newPublicIP := &pb.IP{ newPublicIP := &pb.IP{
Ip: []byte{1, 1, 1, 1}, Ip: []byte{1, 1, 1, 1},
@@ -391,7 +377,7 @@ func TestUpdateMachine(t *testing.T) {
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify both changes persisted // Verify both changes persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name) assert.Equal(t, newName, inspected.Machine.Name)
@@ -399,7 +385,7 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update non-existent machine", func(t *testing.T) { t.Run("update non-existent machine", func(t *testing.T) {
// Try to update properties on a machine that doesn't exist // Try to update properties on a machine that doesn't exist.
nonExistentName := "should-be-updated" nonExistentName := "should-be-updated"
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: "non-existent-machine-id", MachineId: "non-existent-machine-id",
@@ -410,7 +396,7 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update to duplicate name", func(t *testing.T) { t.Run("update to duplicate name", func(t *testing.T) {
// Get two machines // Get two machines.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
@@ -418,7 +404,7 @@ func TestUpdateMachine(t *testing.T) {
machine1 := machines[0] machine1 := machines[0]
machine2 := machines[1] machine2 := machines[1]
// Try to update machine2 with machine1's name // Try to update machine2 with machine1's name.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: machine2.Machine.Id, MachineId: machine2.Machine.Id,
Name: &machine1.Machine.Name, Name: &machine1.Machine.Name,
@@ -428,20 +414,20 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update with empty request", func(t *testing.T) { t.Run("update with empty request", func(t *testing.T) {
// Get a machine // Get a machine.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
targetMachine := machines[0] targetMachine := machines[0]
// Update with no fields set (should be a no-op) // Update with no fields set (should be a no-op).
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
} }
updatedMachine, err := cli.UpdateMachine(ctx, req) updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err) require.NoError(t, err)
// Machine should remain unchanged // Machine should remain unchanged.
assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name) assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name)
if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil { if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil {
assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip)
+153 -2
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"regexp"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -1414,7 +1415,7 @@ myapp.example.com {
} }
}) })
monitorPeriod := 1 * time.Second monitorPeriod := 3 * time.Second
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Name: name, Name: name,
Mode: api.ServiceModeReplicated, Mode: api.ServiceModeReplicated,
@@ -1454,7 +1455,8 @@ myapp.example.com {
} }
}) })
monitorPeriod := 1 * time.Second // 1 second is sometimes not enough for Docker to detect the crash and start restarting the container.
monitorPeriod := 3 * time.Second
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Name: name, Name: name,
Mode: api.ServiceModeReplicated, Mode: api.ServiceModeReplicated,
@@ -2021,4 +2023,153 @@ func TestServiceLifecycle(t *testing.T) {
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "machines not found") assert.Contains(t, err.Error(), "machines not found")
}) })
t.Run("internal DNS", func(t *testing.T) {
t.Parallel()
// Deploy a global service so each machine gets one container.
serviceName := "test-dns-service"
t.Cleanup(func() {
err := cli.RemoveService(ctx, serviceName)
if err != nil && !errors.Is(err, api.ErrNotFound) {
require.NoError(t, err)
}
})
spec := api.ServiceSpec{
Name: serviceName,
Mode: api.ServiceModeGlobal,
Container: api.ContainerSpec{
Image: "portainer/pause:latest",
},
}
_, err := cli.RunService(ctx, spec)
require.NoError(t, err)
svc, err := cli.InspectService(ctx, serviceName)
require.NoError(t, err)
// Deploy a test service to run DNS queries from.
queryServiceName := "test-dns-query-service"
t.Cleanup(func() {
err := cli.RemoveService(ctx, queryServiceName)
if err != nil && !errors.Is(err, api.ErrNotFound) {
require.NoError(t, err)
}
})
querySvcSpec := api.ServiceSpec{
Name: queryServiceName,
Mode: api.ServiceModeReplicated,
Replicas: 1,
Placement: api.Placement{
Machines: []string{c.Machines[0].Name},
},
Container: api.ContainerSpec{
Image: "alpine:3.20",
Command: []string{"sleep", "infinity"},
},
}
_, err = cli.RunService(ctx, querySvcSpec)
require.NoError(t, err)
querySvc, err := cli.InspectService(ctx, queryServiceName)
require.NoError(t, err)
queryContainer := querySvc.Containers[0]
runNslookup := func(t *testing.T, dnsQuery string) string {
dnsOutput, err := execInContainerAndReadOutput(
t, ctx, cli, queryServiceName, queryContainer.Container.ID,
[]string{"nslookup", dnsQuery},
)
require.NoError(t, err)
return dnsOutput
}
assertNoDNSErrors := func(t *testing.T, dnsOutput string) {
assert.NotContains(t, dnsOutput, "server can't find", "DNS query should not contain NXDOMAIN/SERVFAIL errors")
}
t.Run("service name resolves to all container IPs", func(t *testing.T) {
dnsOutput := runNslookup(t, serviceName+".internal")
t.Logf("DNS query output:\n%s", dnsOutput)
for _, ctr := range svc.Containers {
containerIP := ctr.Container.UncloudNetworkIP().String()
assert.Contains(t, dnsOutput, containerIP,
"Service DNS should resolve to container IP %s", containerIP)
}
assertNoDNSErrors(t, dnsOutput)
})
t.Run("machine-specific service DNS lookups", func(t *testing.T) {
for _, targetContainer := range svc.Containers {
targetMachineID := targetContainer.MachineID
targetContainerIP := targetContainer.Container.UncloudNetworkIP().String()
machineSpecificDNS := targetMachineID + ".m." + serviceName + ".internal"
dnsOutput := runNslookup(t, machineSpecificDNS)
t.Logf("Machine-specific DNS query output for %s:\n%s", machineSpecificDNS, dnsOutput)
assert.Contains(t, dnsOutput, targetContainerIP,
"Machine-specific DNS %s should resolve to container IP %s",
machineSpecificDNS, targetContainerIP)
// Machine-specific lookup should return only the container on that machine.
for _, ctr := range svc.Containers {
if ctr.MachineID != targetMachineID {
otherContainerIP := ctr.Container.UncloudNetworkIP().String()
assert.NotContains(t, dnsOutput, otherContainerIP,
"Machine-specific DNS %s should not resolve to other container IP %s",
machineSpecificDNS, otherContainerIP)
}
}
assertNoDNSErrors(t, dnsOutput)
}
})
t.Run("service ID DNS lookup", func(t *testing.T) {
dnsOutput := runNslookup(t, svc.ID+".internal")
t.Logf("Service ID DNS query output:\n%s", dnsOutput)
for _, ctr := range svc.Containers {
containerIP := ctr.Container.UncloudNetworkIP().String()
assert.Contains(t, dnsOutput, containerIP,
"Service ID DNS should resolve to container IP %s", containerIP)
}
assertNoDNSErrors(t, dnsOutput)
})
t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) {
var localIP string
for _, ctr := range svc.Containers {
if ctr.MachineID == queryContainer.MachineID {
localIP = ctr.Container.UncloudNetworkIP().String()
break
}
}
require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryContainer.MachineID)
// Extracts the first IP from nslookup output after the "Name:" line.
re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`)
// The default mode randomizes IP order, so run multiple times to reduce the chance of passing by coincidence.
for range 5 {
dnsOutput := runNslookup(t, "nearest."+serviceName+".internal")
t.Logf("Nearest mode DNS query output:\n%s", dnsOutput)
matches := re.FindStringSubmatch(dnsOutput)
require.Len(t, matches, 2, "Should find Name's Address in DNS output")
firstIP := matches[1]
assert.Equal(t, localIP, firstIP,
"Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)",
queryContainer.MachineID, localIP, firstIP)
assertNoDNSErrors(t, dnsOutput)
}
})
})
} }
@@ -76,9 +76,6 @@ is typically enough for a process in a container to initialise all its dependenc
You can change the monitoring period for a service with `deploy.update_config.monitor`. For example, increase it if your You can change the monitoring period for a service with `deploy.update_config.monitor`. For example, increase it if your
app takes longer to start or if you want to give it more time to recover from transient errors on startup. app takes longer to start or if you want to give it more time to recover from transient errors on startup.
Set it to `0s` to skip monitoring entirely if you are confident the new containers will start correctly and want to
speed up the deployment.
```yaml title="compose.yaml" ```yaml title="compose.yaml"
services: services:
app: app:
@@ -89,6 +86,9 @@ services:
monitor: 10s monitor: 10s
``` ```
Set it to `0s` to skip monitoring entirely if you are confident the new containers will start correctly and want to
speed up the deployment. A safer alternative is to configure a [health check](#health-checks) instead.
You can also change the default monitoring period (`5s`) for all services globally with an environment variable You can also change the default monitoring period (`5s`) for all services globally with an environment variable
`UNCLOUD_HEALTH_MONITOR_PERIOD`: `UNCLOUD_HEALTH_MONITOR_PERIOD`:
@@ -99,14 +99,14 @@ export UNCLOUD_HEALTH_MONITOR_PERIOD=10s
export UNCLOUD_HEALTH_MONITOR_PERIOD=0s export UNCLOUD_HEALTH_MONITOR_PERIOD=0s
``` ```
`deploy.update_config.monitor` overrides the global default for each service. `deploy.update_config.monitor` overrides the global default for that service.
### Health checks ### Health checks
If your container has a [`healthcheck`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#healthcheck) If your service has a [`healthcheck`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#healthcheck)
configured, Uncloud also checks its health status during and after the monitoring period. configured, Uncloud also checks its health status during and after the monitoring period.
If the container becomes `healthy` before the monitoring period ends, the deployment succeeds early and moves on to the If a container becomes `healthy` before the monitoring period ends, the deployment succeeds early and moves on to the
next container. If the container is `unhealthy` after the monitoring period, Uncloud next container. If the container is `unhealthy` after the monitoring period, Uncloud
[rolls it back](#rollback-on-failure) and fails the deployment. Transient `unhealthy` states during the monitoring [rolls it back](#rollback-on-failure) and fails the deployment. Transient `unhealthy` states during the monitoring
period are tolerated to give the container time to recover from startup issues. period are tolerated to give the container time to recover from startup issues.
@@ -3,68 +3,69 @@
Uncloud supports a subset of the [Compose specification](https://compose-spec.io/) with some extensions and limitations. Uncloud supports a subset of the [Compose specification](https://compose-spec.io/) with some extensions and limitations.
The following table shows the support status for main Compose features: The following table shows the support status for main Compose features:
| Feature | Support Status | Notes | | Feature | Support Status | Notes |
|--------------------|--------------------|----------------------------------------------------------------------------------------------------------------| |---------------------|--------------------|----------------------------------------------------------------------------------------------------------------|
| **Services** | | | | **Services** | | |
| `build` | ✅ Supported | Build context and Dockerfile | | `build` | ✅ Supported | Build context and Dockerfile |
| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) | | `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) |
| `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop | | `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop |
| `command` | ✅ Supported | Override container command | | `command` | ✅ Supported | Override container command |
| `configs` | ✅ Supported | File-based and inline configs | | `configs` | ✅ Supported | File-based and inline configs |
| `cpus` | ✅ Supported | CPU limit | | `cpus` | ✅ Supported | CPU limit |
| `depends_on` | ⚠️ Limited | Services deployed in order but conditions not checked | | `depends_on` | ⚠️ Limited | Services deployed in order but conditions not checked |
| `devices` | ✅ Supported | Device mappings | | `devices` | ✅ Supported | Device mappings |
| `dns` | ❌ Not supported | Built-in service discovery | | `dns` | ❌ Not supported | Built-in service discovery |
| `dns_search` | ❌ Not supported | Built-in service discovery | | `dns_search` | ❌ Not supported | Built-in service discovery |
| `entrypoint` | ✅ Supported | Override container entrypoint | | `entrypoint` | ✅ Supported | Override container entrypoint |
| `env_file` | ✅ Supported | Environment file | | `env_file` | ✅ Supported | Environment file |
| `environment` | ✅ Supported | Environment variables | | `environment` | ✅ Supported | Environment variables |
| `gpus` | ✅ Supported | GPU device access | | `gpus` | ✅ Supported | GPU device access |
| `healthcheck` | ✅ Supported | Health check configuration | | `healthcheck` | ✅ Supported | Health check configuration |
| `image` | ✅ Supported | Container image specification | | `image` | ✅ Supported | Container image specification |
| `init` | ✅ Supported | Run init process in container | | `init` | ✅ Supported | Run init process in container |
| `labels` | ❌ Not supported | | | `labels` | ❌ Not supported | |
| `links` | ❌ Not supported | Use service names for communication | | `links` | ❌ Not supported | Use service names for communication |
| `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver | | `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver |
| `mem_limit` | ✅ Supported | Memory limit | | `mem_limit` | ✅ Supported | Memory limit |
| `mem_reservation` | ✅ Supported | Memory reservation | | `mem_reservation` | ✅ Supported | Memory reservation |
| `mem_swappiness` | ❌ Not supported | | | `mem_swappiness` | ❌ Not supported | |
| `memswap_limit` | ❌ Not supported | | | `memswap_limit` | ❌ Not supported | |
| `networks` | ❌ Not supported | All containers share cluster network | | `networks` | ❌ Not supported | All containers share cluster network |
| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](#x-ports) for HTTP/HTTPS | | `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](#x-ports) for HTTP/HTTPS |
| `privileged` | ✅ Supported | Run containers in privileged mode | | `privileged` | ✅ Supported | Run containers in privileged mode |
| `pull_policy` | ✅ Supported | `always`, `missing`, `never` | | `pull_policy` | ✅ Supported | `always`, `missing`, `never` |
| `secrets` | ❌ Not supported | Use configs or environment variables | | `secrets` | ❌ Not supported | Use configs or environment variables |
| `security_opt` | ❌ Not supported | | | `security_opt` | ❌ Not supported | |
| `storage_opt` | ❌ Not supported | | | `stop_grace_period` | ✅ Supported | Time to wait after SIGTERM before SIGKILL |
| `sysctls` | ✅ Supported | Namespaced kernel parameters | | `storage_opt` | ❌ Not supported | |
| `user` | ✅ Supported | Set container user | | `sysctls` | ✅ Supported | Namespaced kernel parameters |
| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | | `user` | ✅ Supported | Set container user |
| **Deploy** | | | | `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs |
| `labels` | ❌ Not supported | | | **Deploy** | | |
| `mode` | ✅ Supported | Either `global` or `replicated` | | `labels` | ❌ Not supported | |
| `placement` | ❌ Not supported | Use [`x-machines`](#x-machines) extension | | `mode` | ✅ Supported | Either `global` or `replicated` |
| `replicas` | ✅ Supported | Number of container replicas | | `placement` | ❌ Not supported | Use [`x-machines`](#x-machines) extension |
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations | | `replicas` | ✅ Supported | Number of container replicas |
| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` | | `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
| `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) | | `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) | | `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) |
| **Volumes** | | | | `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
| Named volumes | ✅ Supported | Docker volumes | | **Volumes** | | |
| Bind mounts | ✅ Supported | Host path binding | | Named volumes | ✅ Supported | Docker volumes |
| Tmpfs mounts | ✅ Supported | In-memory filesystems | | Bind mounts | ✅ Supported | Host path binding |
| Volume labels | ✅ Supported | Custom labels | | Tmpfs mounts | ✅ Supported | In-memory filesystems |
| External volumes | ✅ Supported | Must exist before deployment | | Volume labels | ✅ Supported | Custom labels |
| Volume drivers | ⚠️ Limited | Local driver only | | External volumes | ✅ Supported | Must exist before deployment |
| **Configs** | | | | Volume drivers | ⚠️ Limited | Local driver only |
| File-based configs | ✅ Supported | Read from file | | **Configs** | | |
| Inline configs | ✅ Supported | Defined in compose file | | File-based configs | ✅ Supported | Read from file |
| External configs | ❌ Not supported | Not supported | | Inline configs | ✅ Supported | Defined in compose file |
| Short syntax | ❌ Not supported | Use long syntax only | | External configs | ❌ Not supported | Not supported |
| **Extensions** | | | | Short syntax | ❌ Not supported | Use long syntax only |
| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | | **Extensions** | | |
| `x-machines` | ✅ Uncloud-specific | Machine placement constraints | | `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration |
| `x-ports` | ✅ Uncloud-specific | Service port publishing | | `x-machines` | ✅ Uncloud-specific | Machine placement constraints |
| `x-ports` | ✅ Uncloud-specific | Service port publishing |
### Legend ### Legend
@@ -25,6 +25,5 @@ Manage machines in the cluster.
* [uc machine ls](uc_machine_ls.md) - List machines in a cluster. * [uc machine ls](uc_machine_ls.md) - List machines in a cluster.
* [uc machine rename](uc_machine_rename.md) - Rename a machine in the cluster. * [uc machine rename](uc_machine_rename.md) - Rename a machine in the cluster.
* [uc machine rm](uc_machine_rm.md) - Remove a machine from a cluster and reset it. * [uc machine rm](uc_machine_rm.md) - Remove a machine from a cluster and reset it.
* [uc machine token](uc_machine_token.md) - Print the local machine's token for adding it to a cluster.
* [uc machine update](uc_machine_update.md) - Update machine configuration in the cluster. * [uc machine update](uc_machine_update.md) - Update machine configuration in the cluster.
+13 -9
View File
@@ -17,15 +17,19 @@ uc machine add [USER@]HOST[:PORT] [flags]
## Options ## Options
``` ```
-h, --help help for add -h, --help help for add
-n, --name string Assign a name to the machine. -n, --name string Assign a name to the machine.
--no-caddy Don't deploy Caddy reverse proxy service to the machine. --no-caddy Don't deploy Caddy reverse proxy service to the machine.
--no-install Skip installation of Docker, Uncloud daemon, and dependencies on the machine. Assumes they're already installed and running. --no-install Skip installation of Docker, Uncloud daemon, and dependencies on the machine. Assumes they're already installed and running.
--public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto") --public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto")
-i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519") -i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519")
--version string Version of the Uncloud daemon to install on the machine. (default "latest") --version string Version of the Uncloud daemon to install on the machine. (default "latest")
-y, --yes Auto-confirm prompts (e.g., resetting an already initialised machine). --wg-endpoint strings WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port 51820 is used if omitted.
Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM] Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.
Multiple endpoints can be specified by repeating the flag or using a comma-separated list.
If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.
-y, --yes Auto-confirm prompts (e.g., resetting an already initialised machine).
Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]
``` ```
## Options inherited from parent commands ## Options inherited from parent commands
@@ -46,6 +46,10 @@ uc machine init [schema://]USER@HOST[:PORT] [flags]
--public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto") --public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto")
-i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519") -i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519")
--version string Version of the Uncloud daemon to install on the machine. (default "latest") --version string Version of the Uncloud daemon to install on the machine. (default "latest")
--wg-endpoint strings WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port 51820 is used if omitted.
Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.
Multiple endpoints can be specified by repeating the flag or using a comma-separated list.
If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.
-y, --yes Auto-confirm prompts (e.g., resetting an already initialised machine). -y, --yes Auto-confirm prompts (e.g., resetting an already initialised machine).
Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM] Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]
``` ```
@@ -1,28 +0,0 @@
# uc machine token
Print the local machine's token for adding it to a cluster.
```
uc machine token [flags]
```
## Options
```
-d, --data-dir string Directory for storing persistent machine state. (default "/var/lib/uncloud")
-h, --help help for token
```
## 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+cli://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock
-c, --context string Name of the cluster context to use (default is the current context). [$UNCLOUD_CONTEXT]
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```
## See also
* [uc machine](uc_machine.md) - Manage machines in the cluster.
@@ -6,22 +6,41 @@ Update machine configuration in the cluster.
Update machine configuration in the cluster. Update machine configuration in the cluster.
This command allows setting various machine properties including: Change the name, public IP address, or WireGuard endpoints of an existing machine.
- Machine name (--name) At least one flag must be specified to perform an update.
- Public IP address (--public-ip)
At least one flag must be specified to perform an update operation.
``` ```
uc machine update [flags] uc machine update MACHINE [flags]
```
## Examples
```
# Rename a machine.
uc machine update machine1 --name web-server
# Set the public IP address of a machine.
uc machine update machine1 --public-ip 203.0.113.10
# Remove the public IP address from a machine.
uc machine update machine1 --public-ip none
# Update WireGuard endpoints for a machine.
uc machine update machine1 --wg-endpoint 203.0.113.10 --wg-endpoint 192.168.1.5
# Update multiple properties at once.
uc machine update machine1 --name web-server --public-ip 203.0.113.10
``` ```
## Options ## Options
``` ```
-h, --help help for update -h, --help help for update
--name string New name for the machine --name string New name for the machine
--public-ip string Public IP address of the machine for ingress configuration. Use 'none' or '' to remove the public IP. --public-ip string Public IP address of the machine for ingress configuration. Use 'none' or '' to remove the public IP.
--wg-endpoint strings WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port 51820 is used if omitted.
Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.
Multiple endpoints can be specified by repeating the flag or using a comma-separated list.
``` ```
## Options inherited from parent commands ## Options inherited from parent commands