Compare commits

...
33 Commits
Author SHA1 Message Date
Pasha Sviderski 1caa5ccf0e docs: generate CLI reference docs for wg commands 2026-01-10 18:37:40 +11:00
Pasha Sviderski 76affeb6b2 chore: minor formatting for 'wg show' command, enrich Unimplemented error with >=0.16 requirement 2026-01-10 18:36:56 +11:00
Justin BradfordandGitHub a945291371 feat: Add uc wg show command to inspect a machine's uncloud wireguard network (#161)
* feat: Add `uc wg show` command to inspect a machine's uncloud wireguard network (draft / work in progress)

* Compile proto files with `make proto-mise`

* Fix lint errors

* Output peers in tabwriter table

* Lookup machine name for wireguard peer public key

* Add --machine flag to `uc wg show` to proxy call to specific machine

* Small refactor to move wg show logic out into a separate function
2026-01-10 16:49:35 +11:00
06c3fba96e feat: add support for sysctls compose key (#239)
* feat: add support for `sysctls` compose key

* lint

---------

Co-authored-by: Pasha Sviderski <me@psviderski.name>
2026-01-10 15:53:56 +11:00
Pasha Sviderski 7e739dd2f0 docs: update compose matrix 'build' supported, link capabilities man page 2026-01-10 15:27:42 +11:00
Andrey ViktorovandGitHub 6c2f85d38a feat: add support for cap_add and cap_drop compose keys (#238) 2026-01-10 15:13:03 +11:00
Pasha Sviderski 1ea1d15845 docs: update README subtitle 2026-01-04 16:22:46 +10:00
Anton OvchinnikovandGitHub 5e0d4d0f05 build: Generate CLI docs in GHA (#236)
* build: Generate CLI docs in GHA
* fix: Move no-tty logic outside cobra definitions
2026-01-02 10:23:42 +10:00
Anton OvchinnikovandGitHub 9a1d2e0d46 fix: Equals and Clone for configs (#235) 2025-12-30 20:32:19 +10:00
Anton Ovchinnikov 878f96a4b7 Revert "build: Generate CLI docs in GHA"
This reverts commit 9efaab2beb.
2025-12-30 01:51:31 +01:00
Anton Ovchinnikov 9efaab2beb build: Generate CLI docs in GHA 2025-12-30 01:22:56 +01:00
Anton OvchinnikovandGitHub 2c34ba7eff fix(configs): Create non-existent parent directories automatically (#233) 2025-12-29 11:38:06 +01:00
Anton Ovchinnikov 57e2e22266 docs: Add missing docs for uc start/stop 2025-12-28 17:01:01 +01:00
Anton OvchinnikovandGitHub 4ec5c15254 docs: Link to iximiuz Labs tutorials (#230) 2025-12-27 15:33:12 +10:00
Anton Ovchinnikov 33a3683196 chore: Regenerate CLI docs 2025-12-26 12:23:59 +01:00
Pasha Sviderski 4be1ea3506 fix: machine name for failed machines in 'uc ps' 2025-12-23 19:49:21 +10:00
Pasha Sviderski f86e63c851 chore: reduce the default gRPC connection timeout to proxied machines 20->10s 2025-12-23 19:48:57 +10:00
Pasha Sviderski f6cf73190f chore: wait for FailedPrecondition in addition to Unavailable for backward compatibility 2025-12-23 19:48:18 +10:00
Pasha Sviderski 5308c87651 chore: update 'uc machine init/add' to wait for cluster readiness, confirm caddy deployment on added machine 2025-12-23 19:47:26 +10:00
Pasha Sviderski 9bd5ffcd2d chore: update ucind cluster to wait for initial store sync and cluster readiness 2025-12-23 19:02:04 +10:00
Pasha Sviderski 8d8acd5410 chore: wait for the initial store sync and cluster components before serving cluster gRPC requests 2025-12-23 19:00:09 +10:00
Pasha Sviderski 4d97c30a9c chore: enable gRPC auto retries for transient Unavailable failures up to ~8s 2025-12-23 18:54:08 +10:00
Pasha Sviderski aa71ab0220 fix: e2e caddy test after changing the config header 2025-12-22 21:22:56 +10:00
Pasha Sviderski 2345ee9856 docs: improve Caddyfile header and note about misconfigured caddy service, minor docs update 2025-12-22 16:31:15 +10:00
Justin BradfordandGitHub 65e9cdd444 refactor: use WaitGroup.Go to replace wg.Add(1)/go/wg.Done() boilerplate (#222)
https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_waitgroup
2025-12-19 21:07:29 +10:00
Pasha Sviderski 85a2615db1 chore: add 'ctx conn' alias for 'ctx connection' command 2025-12-19 20:53:30 +10:00
Pasha Sviderski 777615bc56 chore: add TODO to remove extra checks after upgrading clusters to perform store sync 2025-12-19 20:52:46 +10:00
Pasha Sviderski bac34ebd0c chore: save machine state file atomically 2025-12-19 20:12:46 +10:00
Pasha Sviderski b2322e1c49 fix: WireGuard peers misconfiguration on joined machine by waiting for the initial store sync 2025-12-19 19:52:13 +10:00
Pasha Sviderski 436671457d chore: pass current store DB version when adding new machine to cluster 2025-12-19 18:51:49 +10:00
Pasha Sviderski fc847093d2 chore: new InspectMachine gRPC method that to return store DB version (lamport time) 2025-12-19 16:29:14 +10:00
Pasha Sviderski d2a7af744d fix: uncloud daemon crash when listing partially replicated container records 2025-12-18 16:32:26 +10:00
Pasha Sviderski 5acf557675 chore: add --yes flag to 'uc machine init/add' to auto-confirm machine reset 2025-12-17 17:36:54 +10:00
70 changed files with 2368 additions and 576 deletions
+6
View File
@@ -36,6 +36,12 @@ jobs:
git diff --exit-code || git diff --exit-code ||
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1) (echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
- name: Generate docs
run: |
make cli-docs
git diff --exit-code ||
(echo "Documentation is out of date. Please run 'make cli-docs' and commit the changes." && exit 1)
- name: Run tests - name: Run tests
run: | run: |
make ucind-image make ucind-image
+2
View File
@@ -7,6 +7,8 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
/uncloud
/uncloudd
# OS X # OS X
.DS_Store .DS_Store
+11 -1
View File
@@ -1,7 +1,7 @@
<div align="center"> <div align="center">
<img src="./website/landing/images/logo-title.svg#gh-light-mode-only" alt="Uncloud logo"/> <img src="./website/landing/images/logo-title.svg#gh-light-mode-only" alt="Uncloud logo"/>
<img src="./website/landing/images/logo-title-dark.svg#gh-dark-mode-only" alt="Uncloud logo"/> <img src="./website/landing/images/logo-title-dark.svg#gh-dark-mode-only" alt="Uncloud logo"/>
<p><strong>▸ Docker simplicity. Multi-machine power ◂</strong></p> <p><strong>▸ Deploy and scale containerised apps across servers without Swarm or Kubernetes overhead ◂</strong></p>
<p> <p>
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a> <a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
@@ -281,6 +281,16 @@ using the CLI or directly over SSH. They all have the complete cluster state and
machine is a full backup of your control plane. machine is a full backup of your control plane.
</details> </details>
## 🧪 Interactive tutorials
To give you a chance to play with Uncloud without even leaving your browser, we're providing interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
Available tutorials:
1. [Setting up a new Uncloud cluster](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) - the tutorial walks you through creating a new cluster with two machines and then deploying a simple web service to it.
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you can play with an already initialized Uncloud cluster.
## 🏗 Project status ## 🏗 Project status
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
+1
View File
@@ -12,6 +12,7 @@ import (
func NewConnectionCommand() *cobra.Command { func NewConnectionCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "connection", Use: "connection",
Aliases: []string{"conn"},
Short: "Choose a new default connection for the current context.", Short: "Choose a new default connection for the current context.",
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)
+61 -37
View File
@@ -8,7 +8,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/charmbracelet/huh/spinner"
"github.com/charmbracelet/lipgloss"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
@@ -16,8 +17,6 @@ import (
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
type addOptions struct { type addOptions struct {
@@ -27,6 +26,7 @@ type addOptions struct {
publicIP string publicIP string
sshKey string sshKey string
version string version string
yes bool
} }
func NewAddCommand() *cobra.Command { func NewAddCommand() *cobra.Command {
@@ -41,6 +41,8 @@ Connection methods:
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`, ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
// Determine if SSH CLI needs to be used and strip scheme // Determine if SSH CLI needs to be used and strip scheme
@@ -88,6 +90,9 @@ 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().BoolVarP(&opts.yes, "yes", "y", false,
"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]")
return cmd return cmd
} }
@@ -113,6 +118,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
RemoteMachine: remoteMachine, RemoteMachine: remoteMachine,
SkipInstall: opts.noInstall, SkipInstall: opts.noInstall,
Version: opts.version, Version: opts.version,
AutoConfirm: opts.yes,
}) })
if err != nil { if err != nil {
return err return err
@@ -124,25 +130,37 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
return nil return nil
} }
// Wait for the cluster to be initialised to be able to deploy the Caddy service. // Wait for the cluster to be initialised on the machine to be able to deploy the Caddy service.
fmt.Println("Waiting for the machine to be ready...") err = spinner.New().
fmt.Println() Title(" Waiting for the machine to join the cluster...").
if err = waitClusterInitialised(ctx, machineClient); err != nil { Type(spinner.MiniDot).
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err) Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
TitleStyle(lipgloss.NewStyle()).
ActionWithErr(func(ctx context.Context) error {
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
}).
Run()
if err != nil {
return fmt.Errorf("wait for machine to join the cluster: %w", err)
} }
fmt.Println("Machine joined the cluster.")
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
// that may cause a small downtime.
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines, // Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. Otherwise, use the latest version. // use the deployed image version.
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have // NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
// issues accessing the Machine API of existing machines in the cluster. // issues accessing the Machine API of existing machines in the cluster.
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65. // See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
caddyImage := "" caddyImage := ""
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName) caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil { if err != nil {
if !errors.Is(err, api.ErrNotFound) { if errors.Is(err, api.ErrNotFound) {
// Caddy service is not deployed.
return nil
}
return fmt.Errorf("inspect caddy service: %w", err) return fmt.Errorf("inspect caddy service: %w", err)
} }
} else {
caddyImage = caddySvc.Containers[0].Container.Config.Image caddyImage = caddySvc.Containers[0].Container.Config.Image
// Find the latest created container and use its image. // Find the latest created container and use its image.
var latestCreated time.Time var latestCreated time.Time
@@ -156,47 +174,53 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
caddyImage = c.Container.Config.Image caddyImage = c.Container.Config.Image
} }
} }
}
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
// that may cause a small downtime.
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{}) d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
plan, err := d.Plan(ctx)
if err != nil {
return fmt.Errorf("plan caddy deployment: %w", err)
}
fmt.Println()
if len(plan.Operations) == 0 {
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
} else {
// Initialise a machine and container name resolver to properly format the plan output.
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, caddySvc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Println("caddy deployment plan:")
fmt.Println(plan.Format(resolver))
fmt.Println()
if !opts.yes {
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm deployment: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error { err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil { if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err) return fmt.Errorf("deploy caddy: %w", err)
} }
return nil return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name)) }, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
if err != nil { if err != nil {
return err return err
} }
}
fmt.Println() fmt.Println()
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut()) return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
} }
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(5*time.Minute),
), ctx)
check := func() error {
_, err := client.ListMachines(ctx, nil)
if err == nil {
return nil
}
statusErr := status.Convert(err)
if statusErr.Code() == codes.FailedPrecondition {
return err
}
return backoff.Permanent(err)
}
return backoff.Retry(check, boff)
}
+32 -8
View File
@@ -5,7 +5,10 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"strings" "strings"
"time"
"github.com/charmbracelet/huh/spinner"
"github.com/charmbracelet/lipgloss"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns" "github.com/psviderski/uncloud/cmd/uncloud/dns"
@@ -28,6 +31,7 @@ type initOptions struct {
sshKey string sshKey string
version string version string
context string context string
yes bool
} }
func NewInitCommand() *cobra.Command { func NewInitCommand() *cobra.Command {
@@ -56,6 +60,8 @@ Connection methods:
// TODO: support initialising a cluster on the local machine. // TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
var remoteMachine *cli.RemoteMachine var remoteMachine *cli.RemoteMachine
@@ -82,6 +88,11 @@ Connection methods:
return initCluster(cmd.Context(), uncli, remoteMachine, opts) return initCluster(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVarP(
&opts.context, "context", "c", cli.DefaultContextName,
"Name of the new context to be created in the Uncloud config to manage the cluster.",
)
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint, cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.") "API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
@@ -119,10 +130,9 @@ 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().StringVarP( cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
&opts.context, "context", "c", cli.DefaultContextName, "Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
"Name of the new context to be created in the Uncloud config to manage the cluster.", "Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
)
return cmd return cmd
} }
@@ -159,20 +169,34 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
RemoteMachine: remoteMachine, RemoteMachine: remoteMachine,
SkipInstall: opts.noInstall, SkipInstall: opts.noInstall,
Version: opts.version, Version: opts.version,
AutoConfirm: opts.yes,
}) })
if err != nil { if err != nil {
return err return err
} }
defer client.Close() defer client.Close()
// Since the cluster API needs a few moments to become ready after cluster initialisation,
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
err = spinner.New().
Title(" Waiting for the cluster to be ready...").
Type(spinner.MiniDot).
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
TitleStyle(lipgloss.NewStyle()).
ActionWithErr(func(ctx context.Context) error {
return client.WaitClusterReady(ctx, 1*time.Minute)
}).
Run()
if err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err)
}
fmt.Println("Cluster is ready.")
if opts.noCaddy && opts.noDNS { if opts.noCaddy && opts.noDNS {
return nil return nil
} }
// Deploy the Caddy service to the initialised machine.
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
// after cluster initialisation, we keep the user informed during this wait.
fmt.Println("Waiting for the machine to be ready...")
fmt.Println() fmt.Println()
if !opts.noDNS { if !opts.noDNS {
+2
View File
@@ -14,6 +14,7 @@ import (
"github.com/psviderski/uncloud/cmd/uncloud/machine" "github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uncloud/service" "github.com/psviderski/uncloud/cmd/uncloud/service"
"github.com/psviderski/uncloud/cmd/uncloud/volume" "github.com/psviderski/uncloud/cmd/uncloud/volume"
"github.com/psviderski/uncloud/cmd/uncloud/wg"
"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/fs" "github.com/psviderski/uncloud/internal/fs"
@@ -134,6 +135,7 @@ func main() {
service.NewStartCommand("service"), service.NewStartCommand("service"),
service.NewStopCommand("service"), service.NewStopCommand("service"),
volume.NewRootCommand(), volume.NewRootCommand(),
wg.NewRootCommand(),
) )
cobra.CheckErr(cmd.Execute()) cobra.CheckErr(cmd.Execute())
} }
+7 -7
View File
@@ -205,18 +205,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
var containers []containerInfo var containers []containerInfo
for _, msc := range machineContainers { for _, msc := range machineContainers {
machineName := "unknown"
// Metadata can be nil if the request was broadcasted to only one machine. // Metadata can be nil if the request was broadcasted to only one machine.
if msc.Metadata == nil && len(machineContainers) > 1 { if msc.Metadata == nil && len(machineContainers) > 1 {
return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response") return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
} }
if msc.Metadata != nil && msc.Metadata.Error != "" {
client.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
msc.Metadata.Error))
continue
}
machineName := "unknown"
if msc.Metadata != nil { if msc.Metadata != nil {
var ok bool var ok bool
machineName, ok = machinesNamesByIP[msc.Metadata.Machine] machineName, ok = machinesNamesByIP[msc.Metadata.Machine]
@@ -231,6 +225,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
} }
} }
if msc.Metadata != nil && msc.Metadata.Error != "" {
client.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
msc.Metadata.Error))
continue
}
for _, ctr := range msc.Containers { for _, ctr := range msc.Containers {
if ctr.Container.State == nil || ctr.Container.Config == nil { if ctr.Container.State == nil || ctr.Container.Config == nil {
continue continue
+6 -1
View File
@@ -59,7 +59,7 @@ If the service has multiple replicas and no container ID is specified, the comma
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background") execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !cli.IsStdoutTerminal(), execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", false,
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.") "Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
// Keep "-i" and "-t" flags hidden for compatibility with docker exec // Keep "-i" and "-t" flags hidden for compatibility with docker exec
@@ -82,6 +82,11 @@ If the service has multiple replicas and no container ID is specified, the comma
} }
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error { func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
// Disable TTY allocation if not connected to a terminal
if !cli.IsStdoutTerminal() {
opts.noTty = true
}
if !opts.detach { if !opts.detach {
// Check if we're trying to attach to a TTY from a non-TTY client, e.g. // Check if we're trying to attach to a TTY from a non-TTY client, e.g.
// when doing an 'cmd | uc exec ...' // when doing an 'cmd | uc exec ...'
+132
View File
@@ -0,0 +1,132 @@
package wg
import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "wg",
Short: "Inspect WireGuard network",
}
cmd.AddCommand(newShowCommand())
return cmd
}
type showOptions struct {
machine string
}
func newShowCommand() *cobra.Command {
opts := showOptions{}
cmd := &cobra.Command{
Use: "show",
Short: "Show WireGuard network configuration for a machine.",
Long: "Show the WireGuard network configuration for the machine currently connected to " +
"(or specified by the global --connect flag).",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return runShow(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine to show the configuration for. (default is connected machine)")
return cmd
}
func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
client, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
defer client.Close()
if opts.machine != "" {
// Proxy requests to the specified machine.
ctx, _, err = client.ProxyMachinesContext(ctx, []string{opts.machine})
if err != nil {
return err
}
}
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return fmt.Errorf("inspect WireGuard network: "+
"make sure the target machine is running uncloudd daemon version >= 0.16.0: %w", err)
}
return err
}
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machinesNamesByPublicKey := make(map[string]string)
for _, m := range machines {
publicKey := wgtypes.Key(m.Machine.Network.PublicKey).String()
machinesNamesByPublicKey[publicKey] = m.Machine.Name
}
// Fetch the machine's name for more descriptive output
inspectResp, err := client.MachineClient.InspectMachine(ctx, nil)
if err == nil {
fmt.Printf("Machine name: %s\n", inspectResp.Machines[0].Machine.Name)
}
fmt.Printf("WireGuard interface: %s\n", resp.InterfaceName)
fmt.Printf("WireGuard public key: %s\n", wgtypes.Key(resp.PublicKey).String())
fmt.Printf("WireGuard port: %d\n", resp.ListenPort)
fmt.Println()
if len(resp.Peers) == 0 {
fmt.Println("No WireGuard peers configured.")
return nil
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
if _, err = fmt.Fprintln(tw, "PEER\tPUBLIC KEY\tENDPOINT\tHANDSHAKE\tRECEIVED\tSENT\tALLOWED IPS"); err != nil {
return fmt.Errorf("write header: %w", err)
}
for _, peer := range resp.Peers {
machineName, ok := machinesNamesByPublicKey[wgtypes.Key(peer.PublicKey).String()]
if !ok {
machineName = "(unknown)"
}
lastHandshake := ""
if peer.LastHandshakeTime != nil {
lastHandshake = time.Since(peer.LastHandshakeTime.AsTime()).Round(time.Second).String() + " ago"
}
_, err = fmt.Fprintf(
tw,
"%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
machineName,
wgtypes.Key(peer.PublicKey).String(),
peer.Endpoint,
lastHandshake,
units.HumanSize(float64(peer.ReceiveBytes)),
units.HumanSize(float64(peer.TransmitBytes)),
strings.Join(peer.AllowedIps, ", "),
)
if err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
+28 -2
View File
@@ -162,6 +162,7 @@ type InitClusterOptions struct {
RemoteMachine *RemoteMachine RemoteMachine *RemoteMachine
SkipInstall bool SkipInstall bool
Version string Version string
AutoConfirm bool
} }
// 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.
@@ -197,7 +198,12 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
return nil, fmt.Errorf("inspect machine: %w", err) return nil, fmt.Errorf("inspect machine: %w", err)
} }
if minfo.Id != "" { if minfo.Id != "" {
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil { if !opts.AutoConfirm {
if err = promptResetMachine(); err != nil {
return nil, err
}
}
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
return nil, err return nil, err
} }
} }
@@ -297,6 +303,7 @@ type AddMachineOptions struct {
RemoteMachine *RemoteMachine RemoteMachine *RemoteMachine
SkipInstall bool SkipInstall bool
Version string Version string
AutoConfirm bool
} }
// 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.
@@ -326,6 +333,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
}() }()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first. // Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
// TODO: refactor to use client.InspectMachine.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{}) minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("inspect machine: %w", err) return nil, nil, fmt.Errorf("inspect machine: %w", err)
@@ -342,7 +350,12 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name) return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
} }
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil { if !opts.AutoConfirm {
if err = promptResetMachine(); err != nil {
return nil, nil, err
}
}
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
@@ -393,6 +406,18 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err) return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
} }
// Get the current store DB version from the cluster to pass to the join request.
var storeDBVersion int64
inspectResp, err := c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil {
// TODO(lhf): remove Unimplemented check when v0.17.0 is released.
if status.Convert(err).Code() != codes.Unimplemented {
return nil, nil, fmt.Errorf("inspect current cluster machine: %w", err)
}
} else {
storeDBVersion = inspectResp.Machines[0].StoreDbVersion
}
// Get the most up-to-date list of other machines in the cluster to include them in the join request. // Get the most up-to-date list of other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx, nil) machines, err := c.ListMachines(ctx, nil)
if err != nil { if err != nil {
@@ -409,6 +434,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
joinReq := &pb.JoinClusterRequest{ joinReq := &pb.JoinClusterRequest{
Machine: addResp.Machine, Machine: addResp.Machine,
OtherMachines: otherMachines, OtherMachines: otherMachines,
MinStoreDbVersion: storeDBVersion,
} }
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil { if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
return nil, nil, fmt.Errorf("join cluster: %w", err) return nil, nil, fmt.Errorf("join cluster: %w", err)
+13 -1
View File
@@ -2,6 +2,7 @@ package cli
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -86,7 +87,13 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
return nil return nil
} }
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error { func promptResetMachine() error {
if !IsStdinTerminal() {
return errors.New("the remote machine is already initialised as a cluster member; " +
"cannot ask to confirm reset in non-interactive mode, " +
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
}
var confirm bool var confirm bool
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
@@ -110,10 +117,15 @@ func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) err
return fmt.Errorf("remote machine is already initialised as a cluster member") return fmt.Errorf("remote machine is already initialised as a cluster member")
} }
return nil
}
func resetAndWaitMachine(ctx context.Context, machineClient pb.MachineClient) error {
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil { if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+ return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
"on the remote machine to fully uninstall Uncloud from it", err) "on the remote machine to fully uninstall Uncloud from it", err)
} }
fmt.Println("Resetting the remote machine...") fmt.Println("Resetting the remote machine...")
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil { if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
return fmt.Errorf("wait for machine to be ready after reset: %w", err) return fmt.Errorf("wait for machine to be ready after reset: %w", err)
+567 -167
View File
@@ -10,6 +10,7 @@ import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
) )
@@ -371,6 +372,8 @@ type JoinClusterRequest struct {
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"` Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"` OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"`
// Minimum store database version the new machine should sync to before starting cluster operations.
MinStoreDbVersion int64 `protobuf:"varint,4,opt,name=min_store_db_version,json=minStoreDbVersion,proto3" json:"min_store_db_version,omitempty"`
} }
func (x *JoinClusterRequest) Reset() { func (x *JoinClusterRequest) Reset() {
@@ -419,6 +422,125 @@ func (x *JoinClusterRequest) GetOtherMachines() []*MachineInfo {
return nil return nil
} }
func (x *JoinClusterRequest) GetMinStoreDbVersion() int64 {
if x != nil {
return x.MinStoreDbVersion
}
return 0
}
type InspectMachineResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
Machines []*MachineDetails `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
}
func (x *InspectMachineResponse) Reset() {
*x = InspectMachineResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *InspectMachineResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspectMachineResponse) ProtoMessage() {}
func (x *InspectMachineResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspectMachineResponse.ProtoReflect.Descriptor instead.
func (*InspectMachineResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
}
func (x *InspectMachineResponse) GetMachines() []*MachineDetails {
if x != nil {
return x.Machines
}
return nil
}
type MachineDetails struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
Machine *MachineInfo `protobuf:"bytes,2,opt,name=machine,proto3" json:"machine,omitempty"`
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
StoreDbVersion int64 `protobuf:"varint,3,opt,name=store_db_version,json=storeDbVersion,proto3" json:"store_db_version,omitempty"`
}
func (x *MachineDetails) Reset() {
*x = MachineDetails{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MachineDetails) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineDetails) ProtoMessage() {}
func (x *MachineDetails) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MachineDetails.ProtoReflect.Descriptor instead.
func (*MachineDetails) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
}
func (x *MachineDetails) GetMetadata() *Metadata {
if x != nil {
return x.Metadata
}
return nil
}
func (x *MachineDetails) GetMachine() *MachineInfo {
if x != nil {
return x.Machine
}
return nil
}
func (x *MachineDetails) GetStoreDbVersion() int64 {
if x != nil {
return x.StoreDbVersion
}
return 0
}
type TokenResponse struct { type TokenResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -430,7 +552,7 @@ type TokenResponse struct {
func (x *TokenResponse) Reset() { func (x *TokenResponse) Reset() {
*x = TokenResponse{} *x = TokenResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -443,7 +565,7 @@ func (x *TokenResponse) String() string {
func (*TokenResponse) ProtoMessage() {} func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message { func (x *TokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -456,7 +578,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead. // Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
func (*TokenResponse) Descriptor() ([]byte, []int) { func (*TokenResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
} }
func (x *TokenResponse) GetToken() string { func (x *TokenResponse) GetToken() string {
@@ -475,7 +597,7 @@ type ResetRequest struct {
func (x *ResetRequest) Reset() { func (x *ResetRequest) Reset() {
*x = ResetRequest{} *x = ResetRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -488,7 +610,7 @@ func (x *ResetRequest) String() string {
func (*ResetRequest) ProtoMessage() {} func (*ResetRequest) ProtoMessage() {}
func (x *ResetRequest) ProtoReflect() protoreflect.Message { func (x *ResetRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -501,7 +623,7 @@ func (x *ResetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
func (*ResetRequest) Descriptor() ([]byte, []int) { func (*ResetRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
} }
type Service struct { type Service struct {
@@ -518,7 +640,7 @@ type Service struct {
func (x *Service) Reset() { func (x *Service) Reset() {
*x = Service{} *x = Service{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -531,7 +653,7 @@ func (x *Service) String() string {
func (*Service) ProtoMessage() {} func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message { func (x *Service) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -544,7 +666,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service.ProtoReflect.Descriptor instead. // Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) { func (*Service) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
} }
func (x *Service) GetId() string { func (x *Service) GetId() string {
@@ -586,7 +708,7 @@ type InspectServiceRequest struct {
func (x *InspectServiceRequest) Reset() { func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{} *x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -599,7 +721,7 @@ func (x *InspectServiceRequest) String() string {
func (*InspectServiceRequest) ProtoMessage() {} func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message { func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -612,7 +734,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) { func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{11}
} }
func (x *InspectServiceRequest) GetId() string { func (x *InspectServiceRequest) GetId() string {
@@ -633,7 +755,7 @@ type InspectServiceResponse struct {
func (x *InspectServiceResponse) Reset() { func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{} *x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -646,7 +768,7 @@ func (x *InspectServiceResponse) String() string {
func (*InspectServiceResponse) ProtoMessage() {} func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message { func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -659,7 +781,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) { func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{12}
} }
func (x *InspectServiceResponse) GetService() *Service { func (x *InspectServiceResponse) GetService() *Service {
@@ -669,6 +791,164 @@ func (x *InspectServiceResponse) GetService() *Service {
return nil return nil
} }
type InspectWireGuardNetworkResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
InterfaceName string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"`
PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
ListenPort int32 `protobuf:"varint,3,opt,name=listen_port,json=listenPort,proto3" json:"listen_port,omitempty"`
Peers []*WireGuardPeer `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
}
func (x *InspectWireGuardNetworkResponse) Reset() {
*x = InspectWireGuardNetworkResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *InspectWireGuardNetworkResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspectWireGuardNetworkResponse) ProtoMessage() {}
func (x *InspectWireGuardNetworkResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspectWireGuardNetworkResponse.ProtoReflect.Descriptor instead.
func (*InspectWireGuardNetworkResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{13}
}
func (x *InspectWireGuardNetworkResponse) GetInterfaceName() string {
if x != nil {
return x.InterfaceName
}
return ""
}
func (x *InspectWireGuardNetworkResponse) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
func (x *InspectWireGuardNetworkResponse) GetListenPort() int32 {
if x != nil {
return x.ListenPort
}
return 0
}
func (x *InspectWireGuardNetworkResponse) GetPeers() []*WireGuardPeer {
if x != nil {
return x.Peers
}
return nil
}
type WireGuardPeer struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
LastHandshakeTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_handshake_time,json=lastHandshakeTime,proto3" json:"last_handshake_time,omitempty"`
ReceiveBytes int64 `protobuf:"varint,4,opt,name=receive_bytes,json=receiveBytes,proto3" json:"receive_bytes,omitempty"`
TransmitBytes int64 `protobuf:"varint,5,opt,name=transmit_bytes,json=transmitBytes,proto3" json:"transmit_bytes,omitempty"`
AllowedIps []string `protobuf:"bytes,6,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"`
}
func (x *WireGuardPeer) Reset() {
*x = WireGuardPeer{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *WireGuardPeer) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WireGuardPeer) ProtoMessage() {}
func (x *WireGuardPeer) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WireGuardPeer.ProtoReflect.Descriptor instead.
func (*WireGuardPeer) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{14}
}
func (x *WireGuardPeer) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
func (x *WireGuardPeer) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
func (x *WireGuardPeer) GetLastHandshakeTime() *timestamppb.Timestamp {
if x != nil {
return x.LastHandshakeTime
}
return nil
}
func (x *WireGuardPeer) GetReceiveBytes() int64 {
if x != nil {
return x.ReceiveBytes
}
return 0
}
func (x *WireGuardPeer) GetTransmitBytes() int64 {
if x != nil {
return x.TransmitBytes
}
return 0
}
func (x *WireGuardPeer) GetAllowedIps() []string {
if x != nil {
return x.AllowedIps
}
return nil
}
type Service_Container struct { type Service_Container struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -682,7 +962,7 @@ type Service_Container struct {
func (x *Service_Container) Reset() { func (x *Service_Container) Reset() {
*x = Service_Container{} *x = Service_Container{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -695,7 +975,7 @@ func (x *Service_Container) String() string {
func (*Service_Container) ProtoMessage() {} func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message { func (x *Service_Container) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -708,7 +988,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead. // Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
func (*Service_Container) Descriptor() ([]byte, []int) { func (*Service_Container) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10, 0}
} }
func (x *Service_Container) GetMachineId() string { func (x *Service_Container) GetMachineId() string {
@@ -732,113 +1012,170 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65,
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18,
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08,
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75,
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65,
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03,
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75,
0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65,
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52,
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02,
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72,
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a,
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x69, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52,
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x67, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72,
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11,
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x6d, 0x69, 0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x6e, 0x22, 0x49, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x6c, 0x73, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12,
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61,
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f,
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x64, 0x62, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x52, 0x0e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76,
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 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, 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, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
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, 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, 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, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67,
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 (
@@ -853,7 +1190,7 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_machine_proto_rawDescData return file_internal_machine_api_pb_machine_proto_rawDescData
} }
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*MachineInfo)(nil), // 0: api.MachineInfo (*MachineInfo)(nil), // 0: api.MachineInfo
(*NetworkConfig)(nil), // 1: api.NetworkConfig (*NetworkConfig)(nil), // 1: api.NetworkConfig
@@ -861,49 +1198,64 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest (*InitClusterRequest)(nil), // 3: api.InitClusterRequest
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse (*InitClusterResponse)(nil), // 4: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest (*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
(*TokenResponse)(nil), // 6: api.TokenResponse (*InspectMachineResponse)(nil), // 6: api.InspectMachineResponse
(*ResetRequest)(nil), // 7: api.ResetRequest (*MachineDetails)(nil), // 7: api.MachineDetails
(*Service)(nil), // 8: api.Service (*TokenResponse)(nil), // 8: api.TokenResponse
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest (*ResetRequest)(nil), // 9: api.ResetRequest
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse (*Service)(nil), // 10: api.Service
(*Service_Container)(nil), // 11: api.Service.Container (*InspectServiceRequest)(nil), // 11: api.InspectServiceRequest
(*IP)(nil), // 12: api.IP (*InspectServiceResponse)(nil), // 12: api.InspectServiceResponse
(*IPPrefix)(nil), // 13: api.IPPrefix (*InspectWireGuardNetworkResponse)(nil), // 13: api.InspectWireGuardNetworkResponse
(*IPPort)(nil), // 14: api.IPPort (*WireGuardPeer)(nil), // 14: api.WireGuardPeer
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty (*Service_Container)(nil), // 15: api.Service.Container
(*IP)(nil), // 16: api.IP
(*IPPrefix)(nil), // 17: api.IPPrefix
(*IPPort)(nil), // 18: api.IPPort
(*Metadata)(nil), // 19: api.Metadata
(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 21: google.protobuf.Empty
} }
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig 1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP 16, // 1: api.MachineInfo.public_ip:type_name -> api.IP
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix 17, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP 16, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort 18, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix 17, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
12, // 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 0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo 0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo 0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
11, // 10: api.Service.containers:type_name -> api.Service.Container 7, // 10: api.InspectMachineResponse.machines:type_name -> api.MachineDetails
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service 19, // 11: api.MachineDetails.metadata:type_name -> api.Metadata
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty 0, // 12: api.MachineDetails.machine:type_name -> api.MachineInfo
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest 15, // 13: api.Service.containers:type_name -> api.Service.Container
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest 10, // 14: api.InspectServiceResponse.service:type_name -> api.Service
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty 14, // 15: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty 20, // 16: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest 21, // 17: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest 3, // 18: api.Machine.InitCluster:input_type -> api.InitClusterRequest
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse 5, // 19: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse 21, // 20: api.Machine.Token:input_type -> google.protobuf.Empty
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty 21, // 21: api.Machine.Inspect:input_type -> google.protobuf.Empty
6, // 22: api.Machine.Token:output_type -> api.TokenResponse 21, // 22: api.Machine.InspectMachine:input_type -> google.protobuf.Empty
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo 21, // 23: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty 9, // 24: api.Machine.Reset:input_type -> api.ResetRequest
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse 11, // 25: api.Machine.InspectService:input_type -> api.InspectServiceRequest
19, // [19:26] is the sub-list for method output_type 2, // 26: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
12, // [12:19] is the sub-list for method input_type 4, // 27: api.Machine.InitCluster:output_type -> api.InitClusterResponse
12, // [12:12] is the sub-list for extension type_name 21, // 28: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
12, // [12:12] is the sub-list for extension extendee 8, // 29: api.Machine.Token:output_type -> api.TokenResponse
0, // [0:12] is the sub-list for field type_name 0, // 30: api.Machine.Inspect:output_type -> api.MachineInfo
6, // 31: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse
13, // 32: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse
21, // 33: api.Machine.Reset:output_type -> google.protobuf.Empty
12, // 34: api.Machine.InspectService:output_type -> api.InspectServiceResponse
26, // [26:35] is the sub-list for method output_type
17, // [17:26] is the sub-list for method input_type
17, // [17:17] is the sub-list for extension type_name
17, // [17:17] is the sub-list for extension extendee
0, // [0:17] 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() }
@@ -986,7 +1338,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*TokenResponse); i { switch v := v.(*InspectMachineResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -998,7 +1350,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*ResetRequest); i { switch v := v.(*MachineDetails); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -1010,7 +1362,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*Service); i { switch v := v.(*TokenResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -1022,7 +1374,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i { switch v := v.(*ResetRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -1034,7 +1386,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i { switch v := v.(*Service); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -1046,6 +1398,54 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[12].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[13].Exporter = func(v any, i int) any {
switch v := v.(*InspectWireGuardNetworkResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[14].Exporter = func(v any, i int) any {
switch v := v.(*WireGuardPeer); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[15].Exporter = func(v any, i int) any {
switch v := v.(*Service_Container); i { switch v := v.(*Service_Container); i {
case 0: case 0:
return &v.state return &v.state
@@ -1068,7 +1468,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 12, NumMessages: 16,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+36
View File
@@ -5,6 +5,7 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "internal/machine/api/pb/common.proto"; import "internal/machine/api/pb/common.proto";
service Machine { service Machine {
@@ -13,7 +14,12 @@ service Machine {
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse); rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty); rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse); rpc Token(google.protobuf.Empty) returns (TokenResponse);
// Deprecated: use InspectMachine instead.
rpc Inspect(google.protobuf.Empty) returns (MachineInfo); rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
rpc InspectWireGuardNetwork(google.protobuf.Empty) returns (InspectWireGuardNetworkResponse);
// Reset restores the machine to a clean state, removing all cluster-related configuration and data. // Reset restores the machine to a clean state, removing all cluster-related configuration and data.
rpc Reset(ResetRequest) returns (google.protobuf.Empty); rpc Reset(ResetRequest) returns (google.protobuf.Empty);
@@ -58,6 +64,20 @@ message InitClusterResponse {
message JoinClusterRequest { message JoinClusterRequest {
MachineInfo machine = 1; MachineInfo machine = 1;
repeated MachineInfo other_machines = 3; repeated MachineInfo other_machines = 3;
// Minimum store database version the new machine should sync to before starting cluster operations.
int64 min_store_db_version = 4;
}
message InspectMachineResponse {
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
repeated MachineDetails machines = 1;
}
message MachineDetails {
Metadata metadata = 1;
MachineInfo machine = 2;
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
int64 store_db_version = 3;
} }
message TokenResponse { message TokenResponse {
@@ -88,3 +108,19 @@ message InspectServiceRequest {
message InspectServiceResponse { message InspectServiceResponse {
Service service = 1; Service service = 1;
} }
message InspectWireGuardNetworkResponse {
string interface_name = 1;
bytes public_key = 2;
int32 listen_port = 3;
repeated WireGuardPeer peers = 4;
}
message WireGuardPeer {
bytes public_key = 1;
string endpoint = 2;
google.protobuf.Timestamp last_handshake_time = 3;
int64 receive_bytes = 4;
int64 transmit_bytes = 5;
repeated string allowed_ips = 6;
}
@@ -25,6 +25,8 @@ const (
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster" Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token" Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect" Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectMachine_FullMethodName = "/api.Machine/InspectMachine"
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork"
Machine_Reset_FullMethodName = "/api.Machine/Reset" Machine_Reset_FullMethodName = "/api.Machine/Reset"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService" Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
) )
@@ -38,7 +40,12 @@ type MachineClient interface {
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error) Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
// Deprecated: use InspectMachine instead.
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error) Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error)
// Reset restores the machine to a clean state, removing all cluster-related configuration and data. // Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
@@ -102,6 +109,26 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
return out, nil return out, nil
} }
func (c *machineClient) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectMachineResponse)
err := c.cc.Invoke(ctx, Machine_InspectMachine_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectWireGuardNetworkResponse)
err := c.cc.Invoke(ctx, Machine_InspectWireGuardNetwork_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty) out := new(emptypb.Empty)
@@ -131,7 +158,12 @@ type MachineServer interface {
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error) JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error) Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
// Deprecated: use InspectMachine instead.
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error)
// Reset restores the machine to a clean state, removing all cluster-related configuration and data. // Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
@@ -160,6 +192,12 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) { func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented") return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
} }
func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented")
}
func (UnimplementedMachineServer) InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectWireGuardNetwork not implemented")
}
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) { func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented") return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
} }
@@ -277,6 +315,42 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Machine_InspectMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).InspectMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_InspectMachine_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).InspectMachine(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InspectWireGuardNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).InspectWireGuardNetwork(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_InspectWireGuardNetwork_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).InspectWireGuardNetwork(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetRequest) in := new(ResetRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -340,6 +414,14 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "Inspect", MethodName: "Inspect",
Handler: _Machine_Inspect_Handler, Handler: _Machine_Inspect_Handler,
}, },
{
MethodName: "InspectMachine",
Handler: _Machine_InspectMachine_Handler,
},
{
MethodName: "InspectWireGuardNetwork",
Handler: _Machine_InspectWireGuardNetwork_Handler,
},
{ {
MethodName: "Reset", MethodName: "Reset",
Handler: _Machine_Reset_Handler, Handler: _Machine_Reset_Handler,
+3 -1
View File
@@ -80,6 +80,8 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
backoffConfig.MaxDelay = 15 * time.Second backoffConfig.MaxDelay = 15 * time.Second
var err error var err error
// This client keeps retrying connection in the background indefinitely even after the first connection fails
// and the Unavailable error is returned to the caller.
b.conn, err = grpc.NewClient( b.conn, err = grpc.NewClient(
b.target, b.target,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
@@ -88,7 +90,7 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
// Not published as a constant in gRPC library. // Not published as a constant in gRPC library.
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56 // See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
// Each connection attempt can take up to MinConnectTimeout. // Each connection attempt can take up to MinConnectTimeout.
MinConnectTimeout: 20 * time.Second, MinConnectTimeout: 10 * time.Second,
}), }),
grpc.WithDefaultCallOptions( grpc.WithDefaultCallOptions(
grpc.ForceCodecV2(proxy.Codec()), grpc.ForceCodecV2(proxy.Codec()),
+8 -3
View File
@@ -12,14 +12,16 @@ import (
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
) )
const ( const (
caddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services. caddyfileHeaderFmt = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): %s
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
` `
caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine. caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine.
http:// { http:// {
@@ -56,7 +58,9 @@ https://{{$hostname}} {
}{{end}} }{{end}}
` `
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# or the latest generated config is invalid. Please check the Caddy logs if it's running. # (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
` `
) )
@@ -120,6 +124,7 @@ func (g *CaddyfileGenerator) Generate(
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err) return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
} }
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, time.Now().UTC().Format(time.RFC3339))
if !includeCustom { if !includeCustom {
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
} }
+30 -13
View File
@@ -3,6 +3,7 @@ package caddyconfig
import ( import (
"context" "context"
"errors" "errors"
"regexp"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -17,8 +18,17 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const testCaddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services. // generatedTimestampRegex matches the "(DO NOT EDIT): <RFC3339 timestamp>" part of the header.
# Do not edit manually. Any manual changes will be overwritten on the next update. var generatedTimestampRegex = regexp.MustCompile(`\(DO NOT EDIT\): \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
// normaliseGeneratedTimestamp replaces dynamic timestamps with a placeholder for test comparison.
func normaliseGeneratedTimestamp(caddyfile string) string {
return generatedTimestampRegex.ReplaceAllString(caddyfile, "(DO NOT EDIT): TIMESTAMP_PLACEHOLDER")
}
const testCaddyfileHeader = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# Health check endpoint to verify Caddy reachability on this machine. # Health check endpoint to verify Caddy reachability on this machine.
http:// { http:// {
@@ -190,7 +200,7 @@ https://secure.example.com {
} }
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match") assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
}) })
} }
} }
@@ -216,8 +226,9 @@ func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
time.Now(), time.Now(),
), ),
}, },
want: `# This file is autogenerated by Uncloud based on the configuration of running services. want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'. # User-defined global config from service 'caddy'.
# Global Caddy configuration # Global Caddy configuration
@@ -412,8 +423,9 @@ web.example.com {
time.Now(), time.Now(),
), ),
}, },
want: `# This file is autogenerated by Uncloud based on the configuration of running services. want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'. # User-defined global config from service 'caddy'.
# Global config # Global config
@@ -649,8 +661,9 @@ badconfig.com {
time.Now(), time.Now(),
), ),
}, },
want: `# This file is autogenerated by Uncloud based on the configuration of running services. want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'. # User-defined global config from service 'caddy'.
# Global config from test machine # Global config from test machine
@@ -831,7 +844,7 @@ valid.example.com {
} }
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match") assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
}) })
} }
} }
@@ -926,7 +939,9 @@ http://api.example.com {
} }
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine # NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# or the latest generated config is invalid. Please check the Caddy logs if it's running. # (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`, `,
}, },
{ {
@@ -950,7 +965,9 @@ http://api.example.com {
} }
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine # NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# or the latest generated config is invalid. Please check the Caddy logs if it's running. # (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`, `,
}, },
} }
@@ -964,7 +981,7 @@ http://api.example.com {
config, err := generator.Generate(ctx, tt.containers, false) config, err := generator.Generate(ctx, tt.containers, false)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match") assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
}) })
} }
} }
+170 -51
View File
@@ -41,6 +41,8 @@ type clusterController struct {
dockerCtrl *docker.Controller dockerCtrl *docker.Controller
// dockerReady is signalled when Docker is configured and ready for containers. // dockerReady is signalled when Docker is configured and ready for containers.
dockerReady chan<- struct{} dockerReady chan<- struct{}
// clusterReady is signalled when the cluster controller has finished initializing all components.
clusterReady chan<- struct{}
caddyconfigCtrl *caddyconfig.Controller caddyconfigCtrl *caddyconfig.Controller
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP. // dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
@@ -60,6 +62,7 @@ func newClusterController(
corroService corroservice.Service, corroService corroservice.Service,
dockerService *docker.Service, dockerService *docker.Service,
dockerReady chan<- struct{}, dockerReady chan<- struct{},
clusterReady chan<- struct{},
caddyfileCtrl *caddyconfig.Controller, caddyfileCtrl *caddyconfig.Controller,
dnsServer *dns.Server, dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver, dnsResolver *dns.ClusterResolver,
@@ -81,6 +84,7 @@ func newClusterController(
corroService: corroService, corroService: corroService,
dockerCtrl: docker.NewController(state.ID, dockerService, store), dockerCtrl: docker.NewController(state.ID, dockerService, store),
dockerReady: dockerReady, dockerReady: dockerReady,
clusterReady: clusterReady,
caddyconfigCtrl: caddyfileCtrl, caddyconfigCtrl: caddyfileCtrl,
dnsServer: dnsServer, dnsServer: dnsServer,
dnsResolver: dnsResolver, dnsResolver: dnsResolver,
@@ -123,6 +127,21 @@ func (cc *clusterController) Run(ctx context.Context) error {
errGroup, ctx := errgroup.WithContext(ctx) errGroup, ctx := errgroup.WithContext(ctx)
// Start the WireGuard control loop before waiting for store sync. This ensures endpoint rotation happens
// while waiting, allowing Corrosion to connect to peers.
errGroup.Go(func() error {
if err := cc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
})
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
errGroup.Go(func() error {
cc.handleEndpointChanges(ctx)
return nil
})
// Start the network API server. Assume the management IP can't be changed when the network is running. // Start the network API server. Assume the management IP can't be changed when the network is running.
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort)) apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
listener, err := net.Listen("tcp", apiAddr) listener, err := net.Listen("tcp", apiAddr)
@@ -137,6 +156,22 @@ func (cc *clusterController) Run(ctx context.Context) error {
return nil return nil
}) })
// Wait for the store database to sync to the minimum version before starting store-dependent components.
// This prevents issues with using partially replicated data when the machine just joined the cluster,
// e.g., an empty machine list causing WireGuard peer misconfiguration.
cc.waitStoreSync(ctx)
// Check if waitStoreSync exited because the context was cancelled. Return early in that case.
if ctx.Err() != nil {
cc.stopAPIServer()
err := errGroup.Wait()
if corroErr := cc.stopCorrosion(); corroErr != nil {
err = errors.Join(err, corroErr)
}
return err
}
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Starting embedded DNS resolver.") slog.Info("Starting embedded DNS resolver.")
if err := cc.dnsResolver.Run(ctx); err != nil { if err := cc.dnsResolver.Run(ctx); err != nil {
@@ -169,44 +204,6 @@ func (cc *clusterController) Run(ctx context.Context) error {
return nil return nil
}) })
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
errGroup.Go(func() error {
for {
select {
case e, ok := <-cc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
cc.endpointChanges = nil
return nil
}
cc.state.mu.Lock()
for i := range cc.state.Network.Peers {
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := cc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
cc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
case <-ctx.Done():
return nil
}
}
})
errGroup.Go(func() error {
if err := cc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
})
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Starting caddyconfig controller.") slog.Info("Starting caddyconfig controller.")
if err := cc.caddyconfigCtrl.Run(ctx); err != nil { if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
@@ -225,12 +222,13 @@ func (cc *clusterController) Run(ctx context.Context) error {
}) })
} }
// Wait for the context to be done and stop the network API server. // Signal that the cluster controller has finished starting all components.
close(cc.clusterReady)
slog.Info("Cluster controller finished starting all components.")
// Wait for the context to be done and stop all servers and controllers.
<-ctx.Done() <-ctx.Done()
slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown. cc.stopAPIServer()
cc.server.GracefulStop()
slog.Info("Network API server stopped.")
// Stop the unregistry server with a timeout if it was started. // Stop the unregistry server with a timeout if it was started.
if cc.unregistry != nil { if cc.unregistry != nil {
@@ -248,19 +246,50 @@ func (cc *clusterController) Run(ctx context.Context) error {
// Wait for all controllers to finish. // Wait for all controllers to finish.
err = errGroup.Wait() err = errGroup.Wait()
// It's safe to stop the Corrosion service after the controllers depending on it and API server are stopped. // Stop Corrosion after all controllers depending on it and API server are stopped.
// Use a new context with a timeout as the current context is already canceled. if corroErr := cc.stopCorrosion(); corroErr != nil {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) err = errors.Join(err, corroErr)
defer cancel()
if corroErr := cc.corroService.Stop(ctx); corroErr != nil {
err = errors.Join(err, fmt.Errorf("stop corrosion service: %w", corroErr))
} else {
slog.Info("Corrosion service stopped.")
} }
return err return err
} }
// stopAPIServer gracefully stops the network API server with a timeout.
func (cc *clusterController) stopAPIServer() {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
stopped := make(chan struct{})
go func() {
slog.Info("Stopping network API server.")
cc.server.GracefulStop()
close(stopped)
}()
select {
case <-ctx.Done():
slog.Warn("Network API server graceful stop timed out, forcing stop.", "timeout", timeout)
cc.server.Stop()
case <-stopped:
}
slog.Info("Network API server stopped.")
}
// stopCorrosion stops the Corrosion service with a timeout.
func (cc *clusterController) stopCorrosion() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := cc.corroService.Stop(ctx); err != nil {
return fmt.Errorf("stop corrosion service: %w", err)
}
slog.Info("Corrosion service stopped.")
return nil
}
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers. // ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error { func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil { if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
@@ -281,6 +310,95 @@ func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
return nil return nil
} }
// handleEndpointChanges watches for WireGuard peer endpoint changes and persists them to the machine state.
func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
for {
select {
case e, ok := <-cc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
cc.endpointChanges = nil
return
}
cc.state.mu.Lock()
for i := range cc.state.Network.Peers {
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := cc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
cc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
case <-ctx.Done():
return
}
}
}
// waitStoreSync waits for the store database to sync to the minimum required DB version if set in the machine state.
// Blocks until synced or context is cancelled.
func (cc *clusterController) waitStoreSync(ctx context.Context) {
minVersion := cc.state.MinStoreDBVersion
if minVersion == 0 {
return
}
slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
var (
lastVersion int64
lastLogTime time.Time
lastErrLogTime time.Time
)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
version, err := cc.store.DBVersion(ctx)
if err != nil {
// Log errors at most once every 5 seconds.
if time.Since(lastErrLogTime) >= 5*time.Second {
slog.Error("Failed to get the cluster store DB version, retrying.", "err", err)
lastErrLogTime = time.Now()
}
continue
}
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.
cc.state.mu.Lock()
cc.state.MinStoreDBVersion = 0
if err := cc.state.Save(); err != nil {
slog.Error("Failed to save machine state after the initial cluster store sync.", "err", err)
}
cc.state.mu.Unlock()
return
}
// Log progress only once a second.
if version != lastVersion && time.Since(lastLogTime) >= 1*time.Second {
slog.Info("Syncing cluster store.", "version", version, "min_version", minVersion)
lastLogTime = time.Now()
lastVersion = version
}
}
}
}
// 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 {
@@ -340,6 +458,7 @@ 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.
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 {
+45 -48
View File
@@ -26,12 +26,19 @@ type Cluster struct {
corroAdmin *corrosion.AdminClient corroAdmin *corrosion.AdminClient
// machineID is the ID of the current machine that is running the cluster service. // machineID is the ID of the current machine that is running the cluster service.
machineID string machineID string
// initialised is closed when the machine is configured as a member of a cluster.
initialised <-chan struct{}
// ready is closed when the cluster controller has finished starting all components
// and the machine is ready to serve cluster requests.
ready <-chan struct{}
} }
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster { func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient, initialised, ready <-chan struct{}) *Cluster {
return &Cluster{ return &Cluster{
store: store, store: store,
corroAdmin: corroAdmin, corroAdmin: corroAdmin,
initialised: initialised,
ready: ready,
} }
} }
@@ -41,67 +48,45 @@ func (c *Cluster) UpdateMachineID(mid string) {
} }
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error { func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
initialised, err := c.Initialised(ctx) select {
if err != nil { case <-c.initialised:
return err return fmt.Errorf("cluster is already initialised on this machine")
} default:
if initialised {
return fmt.Errorf("cluster is already initialised")
} }
if err = c.store.Put(ctx, "network", network.String()); err != nil { if err := c.store.Put(ctx, "network", network.String()); err != nil {
return fmt.Errorf("put network to store: %w", err) return fmt.Errorf("put network to store: %w", err)
} }
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil { if err := c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
return fmt.Errorf("put created_at to store: %w", err) return fmt.Errorf("put created_at to store: %w", err)
} }
return nil return nil
} }
func (c *Cluster) Initialised(ctx context.Context) (bool, error) { // checkReady checks if the machine is ready to serve cluster requests (store synced, cluster components started).
var createdAt string func (c *Cluster) checkReady() error {
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil { select {
if errors.Is(err, store.ErrKeyNotFound) { case <-c.ready:
return false, nil
}
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
}
return true, nil
}
func (c *Cluster) checkInitialised(ctx context.Context) error {
initialised, err := c.Initialised(ctx)
if err != nil {
return err
}
if !initialised {
return status.Error(codes.FailedPrecondition, "cluster is not initialised")
}
return nil return nil
} default:
return status.Error(codes.Unavailable, "machine is not ready to serve cluster requests")
func (c *Cluster) Network(ctx context.Context) (netip.Prefix, error) {
if err := c.checkInitialised(ctx); err != nil {
return netip.Prefix{}, err
} }
var net string
if err := c.store.Get(ctx, "network", &net); err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
}
prefix, err := netip.ParsePrefix(net)
if err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
}
return prefix, nil
} }
// AddMachine adds a machine to the cluster. // AddMachine adds a machine to the cluster.
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) { func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
return c.AddMachineWithoutReadyCheck(ctx, req)
}
// AddMachineWithoutReadyCheck adds a machine to the cluster without checking if the cluster is ready.
// This is used internally during cluster initialisation to add the first machine.
func (c *Cluster) AddMachineWithoutReadyCheck(
ctx context.Context, req *pb.AddMachineRequest,
) (*pb.AddMachineResponse, error) {
if req.Network == nil { if req.Network == nil {
return nil, status.Error(codes.InvalidArgument, "network not set") return nil, status.Error(codes.InvalidArgument, "network not set")
} }
@@ -162,7 +147,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey)) manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
} }
// Allocate a subnet for the machine from the cluster network. // Allocate a subnet for the machine from the cluster network.
clusterNetwork, err := c.Network(ctx) clusterNetwork, err := c.network(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err) return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
} }
@@ -198,9 +183,21 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
return resp, nil return resp, nil
} }
func (c *Cluster) network(ctx context.Context) (netip.Prefix, error) {
var net string
if err := c.store.Get(ctx, "network", &net); err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
}
prefix, err := netip.ParsePrefix(net)
if err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
}
return prefix, nil
}
// UpdateMachine updates machine configuration in the cluster. // UpdateMachine updates machine configuration in the cluster.
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) { func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
@@ -283,7 +280,7 @@ func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineReques
// ListMachines lists all machines in the cluster including their membership states. // ListMachines lists all machines in the cluster including their membership states.
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) { func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
@@ -329,7 +326,7 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
// RemoveMachine removes a machine from the cluster. // RemoveMachine removes a machine from the cluster.
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) { func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
+4 -4
View File
@@ -25,7 +25,7 @@ type uncloudDNSDomain struct {
} }
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) { func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
@@ -64,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
} }
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) { func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
@@ -95,7 +95,7 @@ func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
} }
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) { func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
@@ -115,7 +115,7 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
func (c *Cluster) CreateDomainRecords( func (c *Cluster) CreateDomainRecords(
ctx context.Context, req *pb.CreateDomainRecordsRequest, ctx context.Context, req *pb.CreateDomainRecordsRequest,
) (*pb.CreateDomainRecordsResponse, error) { ) (*pb.CreateDomainRecordsResponse, error) {
if err := c.checkInitialised(ctx); err != nil { if err := c.checkReady(); err != nil {
return nil, err return nil, err
} }
+12 -10
View File
@@ -605,6 +605,8 @@ func (s *Server) CreateServiceContainer(
} }
} }
hostConfig := &container.HostConfig{ hostConfig := &container.HostConfig{
CapAdd: spec.Container.CapAdd,
CapDrop: spec.Container.CapDrop,
Binds: spec.Container.Volumes, Binds: spec.Container.Volumes,
Init: spec.Container.Init, Init: spec.Container.Init,
Mounts: mounts, Mounts: mounts,
@@ -621,6 +623,7 @@ func (s *Server) CreateServiceContainer(
RestartPolicy: container.RestartPolicy{ RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyUnlessStopped, Name: container.RestartPolicyUnlessStopped,
}, },
Sysctls: spec.Container.Sysctls,
} }
// Configure the container to use the internal DNS server if it's available. // Configure the container to use the internal DNS server if it's available.
@@ -801,14 +804,17 @@ func (s *Server) injectConfigs(ctx context.Context, containerID string, configs
} }
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API. // copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
// It will create any intermediate directories in the target path that don't exist.
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error { func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
// Create a tar archive containing the file
var buf bytes.Buffer var buf bytes.Buffer
tw := tar.NewWriter(&buf) tw := tar.NewWriter(&buf)
// Create tar header // Trim leading slash(es) to avoid double slashes in the tar path
tarPath := strings.TrimPrefix(targetPath, "/")
// Create tar header with full path
header := &tar.Header{ header := &tar.Header{
Name: filepath.Base(targetPath), Name: tarPath,
Size: int64(len(content)), Size: int64(len(content)),
Mode: int64(fileMode), Mode: int64(fileMode),
ModTime: time.Now(), ModTime: time.Now(),
@@ -834,16 +840,12 @@ func (s *Server) copyContentToContainer(ctx context.Context, containerID string,
return fmt.Errorf("close tar writer: %w", err) return fmt.Errorf("close tar writer: %w", err)
} }
// Copy the tar archive to the container // Always extract to root. The tar archive contains the full path, so tar will
targetDir := filepath.Dir(targetPath) // automatically create any intermediate directories that don't exist in the container.
if targetDir == "." {
targetDir = "/"
}
if err := s.client.CopyToContainer( if err := s.client.CopyToContainer(
ctx, ctx,
containerID, containerID,
targetDir, "/",
&buf, &buf,
container.CopyToContainerOptions{CopyUIDGID: true}, container.CopyToContainerOptions{CopyUIDGID: true},
); err != nil { ); err != nil {
+97 -8
View File
@@ -33,10 +33,12 @@ import (
"github.com/psviderski/unregistry" "github.com/psviderski/unregistry"
"github.com/siderolabs/grpc-proxy/proxy" "github.com/siderolabs/grpc-proxy/proxy"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"golang.zx2c4.com/wireguard/wgctrl"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
) )
const ( const (
@@ -174,10 +176,13 @@ type Machine struct {
state *State state *State
// started is closed when the machine is ready to serve requests on the local API server. // started is closed when the machine is ready to serve requests on the local API server.
started chan struct{} started chan struct{}
// initialised is signalled when the machine is configured as a member of a cluster. // initialised is closed when the machine is configured as a member of a cluster.
initialised chan struct{} initialised chan struct{}
// networkReady is signalled when the Docker network is configured and ready for containers. // networkReady is closed when the Docker network is configured and ready for containers.
networkReady chan struct{} networkReady chan struct{}
// clusterReady is closed when the cluster controller has finished starting all components
// and the machine is ready to serve cluster requests.
clusterReady chan struct{}
// resetting is true when the machine is being reset. // resetting is true when the machine is being reset.
resetting bool resetting bool
// stop cancels the Run method context to stop the machine gracefully. // stop cancels the Run method context to stop the machine gracefully.
@@ -246,7 +251,10 @@ func NewMachine(config *Config) (*Machine, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("create corrosion admin client: %w", err) return nil, fmt.Errorf("create corrosion admin client: %w", err)
} }
c := cluster.NewCluster(corroStore, corroAdmin)
initialised := make(chan struct{})
clusterReady := make(chan struct{})
c := cluster.NewCluster(corroStore, corroAdmin, initialised, clusterReady)
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon. // Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
dbFilePath := filepath.Join(config.DataDir, DBFileName) dbFilePath := filepath.Join(config.DataDir, DBFileName)
@@ -269,8 +277,9 @@ func NewMachine(config *Config) (*Machine, error) {
config: *config, config: *config,
state: state, state: state,
started: make(chan struct{}), started: make(chan struct{}),
initialised: make(chan struct{}, 1), initialised: initialised,
networkReady: make(chan struct{}), networkReady: make(chan struct{}),
clusterReady: clusterReady,
store: corroStore, store: corroStore,
cluster: c, cluster: c,
dockerService: dockerService, dockerService: dockerService,
@@ -294,7 +303,7 @@ func NewMachine(config *Config) (*Machine, error) {
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer) m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer)
if m.Initialised() { if m.Initialised() {
m.initialised <- struct{}{} close(m.initialised)
} }
return m, nil return m, nil
@@ -475,6 +484,7 @@ func (m *Machine) Run(ctx context.Context) error {
m.config.CorrosionService, m.config.CorrosionService,
m.dockerService, m.dockerService,
m.networkReady, m.networkReady,
m.clusterReady,
caddyconfigCtrl, caddyconfigCtrl,
dnsServer, dnsServer,
dnsResolver, dnsResolver,
@@ -722,7 +732,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
addReq.PublicIp = pb.NewIP(publicIP) addReq.PublicIp = pb.NewIP(publicIP)
} }
addResp, err := m.cluster.AddMachine(ctx, addReq) addResp, err := m.cluster.AddMachineWithoutReadyCheck(ctx, addReq)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err) return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
} }
@@ -749,7 +759,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
} }
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name) slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name)
// Signal that the machine is initialised as a member of a cluster. // Signal that the machine is initialised as a member of a cluster.
m.initialised <- struct{}{} close(m.initialised)
resp := &pb.InitClusterResponse{ resp := &pb.InitClusterResponse{
Machine: addResp.Machine, Machine: addResp.Machine,
@@ -792,6 +802,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
PrivateKey: m.state.Network.PrivateKey, PrivateKey: m.state.Network.PrivateKey,
PublicKey: m.state.Network.PublicKey, PublicKey: m.state.Network.PublicKey,
} }
m.state.MinStoreDBVersion = req.MinStoreDbVersion
// Build a peers config from other cluster machines. // Build a peers config from other cluster machines.
m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines)) m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines))
@@ -830,7 +841,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
"peers", len(m.state.Network.Peers), "peers", len(m.state.Network.Peers),
) )
// Signal that the machine is initialised as a member of a cluster. // Signal that the machine is initialised as a member of a cluster.
m.initialised <- struct{}{} close(m.initialised)
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
@@ -864,6 +875,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
return &pb.TokenResponse{Token: tokenStr}, nil return &pb.TokenResponse{Token: tokenStr}, nil
} }
// Deprecated: use InspectMachine instead.
func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) { func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) {
return &pb.MachineInfo{ return &pb.MachineInfo{
Id: m.state.ID, Id: m.state.ID,
@@ -876,6 +888,31 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
}, nil }, nil
} }
func (m *Machine) InspectMachine(ctx context.Context, _ *emptypb.Empty) (*pb.InspectMachineResponse, error) {
dbVersion, err := m.store.DBVersion(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "get database version of the cluster store: %v", err)
}
return &pb.InspectMachineResponse{
Machines: []*pb.MachineDetails{
{
// Metadata is injected by the gRPC proxy.
Machine: &pb.MachineInfo{
Id: m.state.ID,
Name: m.state.Name,
Network: &pb.NetworkConfig{
Subnet: pb.NewIPPrefix(m.state.Network.Subnet),
ManagementIp: pb.NewIP(m.state.Network.ManagementIP),
PublicKey: m.state.Network.PublicKey,
},
},
StoreDbVersion: dbVersion,
},
},
}, nil
}
// IsNetworkReady returns true if the Docker network is ready for containers. // IsNetworkReady returns true if the Docker network is ready for containers.
func (m *Machine) IsNetworkReady() bool { func (m *Machine) IsNetworkReady() bool {
if !m.Initialised() { if !m.Initialised() {
@@ -909,6 +946,58 @@ func (m *Machine) WaitForNetworkReady(ctx context.Context) error {
} }
} }
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
func (m *Machine) InspectWireGuardNetwork(
_ context.Context, _ *emptypb.Empty,
) (*pb.InspectWireGuardNetworkResponse, error) {
deviceName := network.WireGuardInterfaceName
wg, err := wgctrl.New()
if err != nil {
return nil, fmt.Errorf("create WireGuard client: %w", err)
}
defer wg.Close()
dev, err := wg.Device(deviceName)
if err != nil {
return nil, fmt.Errorf("get WireGuard device '%s': %w", deviceName, err)
}
peers := make([]*pb.WireGuardPeer, len(dev.Peers))
for i, p := range dev.Peers {
var lastHandshake *timestamppb.Timestamp
if !p.LastHandshakeTime.IsZero() {
lastHandshake = timestamppb.New(p.LastHandshakeTime)
}
allowedIPs := make([]string, len(p.AllowedIPs))
for j, ip := range p.AllowedIPs {
allowedIPs[j] = ip.String()
}
var endpoint string
if p.Endpoint != nil {
endpoint = p.Endpoint.String()
}
peers[i] = &pb.WireGuardPeer{
PublicKey: p.PublicKey[:],
Endpoint: endpoint,
LastHandshakeTime: lastHandshake,
ReceiveBytes: p.ReceiveBytes,
TransmitBytes: p.TransmitBytes,
AllowedIps: allowedIPs,
}
}
return &pb.InspectWireGuardNetworkResponse{
InterfaceName: dev.Name,
PublicKey: dev.PublicKey[:],
ListenPort: int32(dev.ListenPort),
Peers: peers,
}, nil
}
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related // Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd. // configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) { func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
+37 -2
View File
@@ -24,6 +24,9 @@ type State struct {
Name string Name string
// Network specifies the network configuration for this machine. // Network specifies the network configuration for this machine.
Network *network.Config Network *network.Config
// MinStoreDBVersion is the latest database version of one of the existing cluster machines at the time this machine
// joined the cluster. The machine should sync to at least this version before starting any cluster operations.
MinStoreDBVersion int64 `json:",omitempty"`
// path is the file path config is read from and saved to. // path is the file path config is read from and saved to.
path string path string
@@ -66,7 +69,7 @@ func (c *State) Encode() ([]byte, error) {
return data, nil return data, nil
} }
// Save writes the state data to the file at the given path. // Save atomically writes the state data to the file at the configured path.
func (c *State) Save() error { func (c *State) Save() error {
if c.path == "" { if c.path == "" {
return fmt.Errorf("state path not set") return fmt.Errorf("state path not set")
@@ -80,5 +83,37 @@ func (c *State) Save() error {
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(c.path, data, 0o600)
// Write to a temporary file and rename for atomic save. CreateTemp creates a file with mode 0o600.
tmpFile, err := os.CreateTemp(dir, ".machine.json.*")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
tmpPath := tmpFile.Name()
// Clean up temp file on error.
defer func() {
if err != nil {
os.Remove(tmpPath)
}
}()
if _, err = tmpFile.Write(data); err != nil {
tmpFile.Close()
return fmt.Errorf("write temp file: %w", err)
}
// Without fsync, the data may only be in the OS buffer cache. If the system crashes before the OS flushes
// it to disk, the file could be empty or corrupted even after rename.
if err = tmpFile.Sync(); err != nil {
tmpFile.Close()
return fmt.Errorf("sync temp file: %w", err)
}
if err = tmpFile.Close(); err != nil {
return fmt.Errorf("close temp file: %w", err)
}
if err = os.Rename(tmpPath, c.path); err != nil {
return fmt.Errorf("rename temp file: %w", err)
}
return nil
} }
+35 -6
View File
@@ -82,7 +82,7 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.ServiceCont
// ListContainers returns a list of container records from the store database that match the given options. // ListContainers returns a list of container records from the store database that match the given options.
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) { func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers"). q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced}) Where(sq.Eq{"sync_status": SyncStatusSynced})
if len(opts.MachineIDs) > 0 { if len(opts.MachineIDs) > 0 {
@@ -112,14 +112,23 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
defer rows.Close() defer rows.Close()
var containers []ContainerRecord var containers []ContainerRecord
var cJSON, machineID, syncStatus, updatedAtStr string var id, cJSON, machineID, syncStatus, updatedAtStr string
var updatedAt time.Time var updatedAt time.Time
skipped := 0
for rows.Next() { for rows.Next() {
if err = rows.Scan(&cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil { if err = rows.Scan(&id, &cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
return nil, fmt.Errorf("scan container record: %w", err) return nil, fmt.Errorf("scan container record: %w", err)
} }
// Skip containers with empty JSON data. This can happen during partial replication
// when cr-sqlite has created the row but the container column hasn't been synced yet.
if cJSON == "" || cJSON == "{}" {
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
skipped++
continue
}
var c api.ServiceContainer var c api.ServiceContainer
if err = json.Unmarshal([]byte(cJSON), &c); err != nil { if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
return nil, fmt.Errorf("unmarshal container: %w", err) return nil, fmt.Errorf("unmarshal container: %w", err)
@@ -135,6 +144,11 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
}) })
} }
if skipped > 0 {
slog.Warn("Listing containers from the store skipped empty records (possibly due to partial replication).",
"skipped", skipped, "valid", len(containers))
}
return containers, nil return containers, nil
} }
@@ -166,7 +180,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database. // receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) { func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
// TODO: figure out whether we need sync_status at all (not used at the moment). // TODO: figure out whether we need sync_status at all (not used at the moment).
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers"). q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced}) Where(sq.Eq{"sync_status": SyncStatusSynced})
query, args, err := q.ToSql() query, args, err := q.ToSql()
if err != nil { if err != nil {
@@ -179,15 +193,24 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
} }
var containers []ContainerRecord var containers []ContainerRecord
var cJSON, updatedAtStr string var id, cJSON, updatedAtStr string
skipped := 0
rows := sub.Rows() rows := sub.Rows()
for rows.Next() { for rows.Next() {
var cr ContainerRecord var cr ContainerRecord
if err = rows.Scan(&cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil { if err = rows.Scan(&id, &cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
return nil, nil, err return nil, nil, err
} }
// Skip containers with empty JSON data. This can happen during partial replication
// when cr-sqlite has created the row but the container column hasn't been synced yet.
if cJSON == "" || cJSON == "{}" {
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
skipped++
continue
}
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil { if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
return nil, nil, fmt.Errorf("unmarshal container: %w", err) return nil, nil, fmt.Errorf("unmarshal container: %w", err)
} }
@@ -196,6 +219,12 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
} }
containers = append(containers, cr) containers = append(containers, cr)
} }
if skipped > 0 {
slog.Warn("Container subscription skipped empty records in the store (possibly due to partial replication).",
"skipped", skipped, "valid", len(containers))
}
events, err := sub.Changes() events, err := sub.Changes()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("get subscription changes: %w", err) return nil, nil, fmt.Errorf("get subscription changes: %w", err)
+58 -6
View File
@@ -56,6 +56,25 @@ func (s *Store) Delete(ctx context.Context, key string) error {
return err return err
} }
// DBVersion returns the current cr-sqlite database version (Lamport timestamp).
func (s *Store) DBVersion(ctx context.Context) (int64, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT crsql_db_version()")
if err != nil {
return 0, fmt.Errorf("query crsql_db_version(): %w", err)
}
defer rows.Close()
if !rows.Next() {
return 0, fmt.Errorf("no result from crsql_db_version()")
}
var version int64
if err = rows.Scan(&version); err != nil {
return 0, fmt.Errorf("scan db version: %w", err)
}
return version, 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 {
@@ -116,19 +135,29 @@ func (s *Store) GetMachine(ctx context.Context, machineID string) (*pb.MachineIn
} }
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) { func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name") rows, err := s.corro.QueryContext(ctx, "SELECT id, info FROM machines ORDER BY name")
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var machines []*pb.MachineInfo var machines []*pb.MachineInfo
skipped := 0
for rows.Next() { for rows.Next() {
var mJSON string var id, mJSON string
if err = rows.Scan(&mJSON); err != nil { if err = rows.Scan(&id, &mJSON); err != nil {
return nil, err return nil, err
} }
// Skip machines with empty JSON data. This can happen during partial replication
// when cr-sqlite has created the row but the info column hasn't been synced yet.
if mJSON == "" || mJSON == "{}" {
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
skipped++
continue
}
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true} protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
var m pb.MachineInfo var m pb.MachineInfo
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil { if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
@@ -141,6 +170,12 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
} }
machines = append(machines, &m) machines = append(machines, &m)
} }
if skipped > 0 {
slog.Warn("Listing machines from the store skipped empty records (possibly due to partial replication).",
"skipped", skipped, "valid", len(machines))
}
return machines, nil return machines, nil
} }
@@ -186,24 +221,41 @@ func (s *Store) DeleteMachine(ctx context.Context, id string) error {
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't // SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
// receive any values, it just signals when a machine has been added, updated, or deleted in the database. // receive any values, it just signals when a machine has been added, updated, or deleted in the database.
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) { func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
sub, err := s.corro.SubscribeContext(ctx, "SELECT info FROM machines ORDER BY name", nil, false) sub, err := s.corro.SubscribeContext(ctx, "SELECT id, info FROM machines ORDER BY name", nil, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
rows := sub.Rows() rows := sub.Rows()
var machines []*pb.MachineInfo var machines []*pb.MachineInfo
skipped := 0
for rows.Next() { for rows.Next() {
var mJSON string var id, mJSON string
if err = rows.Scan(&mJSON); err != nil { if err = rows.Scan(&id, &mJSON); err != nil {
return nil, nil, err return nil, nil, err
} }
// Skip machines with empty JSON data. This can happen during partial replication
// when cr-sqlite has created the row but the info column hasn't been synced yet.
if mJSON == "" || mJSON == "{}" {
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
skipped++
continue
}
var m pb.MachineInfo var m pb.MachineInfo
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil { if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err) return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
} }
machines = append(machines, &m) machines = append(machines, &m)
} }
if skipped > 0 {
slog.Warn("Machine subscription skipped empty records in the store (possibly due to partial replication).",
"skipped", skipped, "valid", len(machines))
}
events, err := sub.Changes() events, err := sub.Changes()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("get subscription changes: %w", err) return nil, nil, fmt.Errorf("get subscription changes: %w", err)
+44 -14
View File
@@ -119,17 +119,16 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error { func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
// Init a new cluster on the first machine. // Init a new cluster on the first machine.
initMachine := machines[0] initMachine := machines[0]
if err := WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
}
initClient, err := initMachine.Connect(ctx) initClient, err := initMachine.Connect(ctx)
if err != nil { if err != nil {
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err) return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
} }
defer initClient.Close() defer initClient.Close()
if err := initClient.WaitMachineReady(ctx, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
}
req := &pb.InitClusterRequest{ req := &pb.InitClusterRequest{
MachineName: initMachine.Name, MachineName: initMachine.Name,
Network: pb.NewIPPrefix(cluster.DefaultNetwork), Network: pb.NewIPPrefix(cluster.DefaultNetwork),
@@ -138,14 +137,22 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
if err != nil { if err != nil {
return fmt.Errorf("init cluster: %w", err) return fmt.Errorf("init cluster: %w", err)
} }
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name) fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
fmt.Printf("Waiting for cluster to be ready...")
if err = initClient.WaitClusterReady(ctx, 30*time.Second); err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err)
}
fmt.Println(" done.")
// Get the current store DB version from the init machine to pass to the join requests.
inspectResp, err := initClient.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect init machine: %w", err)
}
// Join the rest of the machines to the cluster. // Join the rest of the machines to the cluster.
for _, m := range machines[1:] { for _, m := range machines[1:] {
if err = WaitMachineReady(ctx, m, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
}
cli, err := m.Connect(ctx) cli, err := m.Connect(ctx)
if err != nil { if err != nil {
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err) return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
@@ -153,6 +160,10 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
//goland:noinspection GoDeferInLoop //goland:noinspection GoDeferInLoop
defer cli.Close() defer cli.Close()
if err := cli.WaitMachineReady(ctx, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
}
tokenResp, err := cli.Token(ctx, &emptypb.Empty{}) tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("get machine token: %w", err) return fmt.Errorf("get machine token: %w", err)
@@ -183,6 +194,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
joinReq := &pb.JoinClusterRequest{ joinReq := &pb.JoinClusterRequest{
Machine: addResp.Machine, Machine: addResp.Machine,
OtherMachines: []*pb.MachineInfo{initResp.Machine}, OtherMachines: []*pb.MachineInfo{initResp.Machine},
MinStoreDbVersion: inspectResp.Machines[0].StoreDbVersion,
} }
if _, err = cli.JoinCluster(ctx, joinReq); err != nil { if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
return fmt.Errorf("join cluster: %w", err) return fmt.Errorf("join cluster: %w", err)
@@ -256,16 +268,16 @@ func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster,
// WaitClusterReady waits for all machines in the cluster to be ready and UP. // WaitClusterReady waits for all machines in the cluster to be ready and UP.
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error { func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
firstMachine := c.Machines[0] firstMachine := c.Machines[0]
if err := WaitMachineReady(ctx, firstMachine, timeout); err != nil {
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
}
cli, err := firstMachine.Connect(ctx) cli, err := firstMachine.Connect(ctx)
if err != nil { if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err) return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
} }
defer cli.Close() defer cli.Close()
if err = cli.WaitClusterReady(ctx, timeout); err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err)
}
boff := backoff.WithContext(backoff.NewExponentialBackOff( boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond), backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second), backoff.WithMaxInterval(1*time.Second),
@@ -289,7 +301,25 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
} }
return nil return nil
} }
return backoff.Retry(checkMachinesUp, boff) if err = backoff.Retry(checkMachinesUp, boff); err != nil {
return err
}
// Wait for each machine to sync the cluster store and be ready to serve cluster requests.
for _, m := range c.Machines[1:] {
mcli, err := m.Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
}
//goland:noinspection GoDeferInLoop
defer mcli.Close()
if err = mcli.WaitClusterReady(ctx, timeout); err != nil {
return fmt.Errorf("wait for cluster to be ready on machine '%s': %w", m.Name, err)
}
}
return nil
} }
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error { func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
-25
View File
@@ -9,7 +9,6 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/cenkalti/backoff/v4"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
@@ -17,7 +16,6 @@ import (
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector" "github.com/psviderski/uncloud/pkg/client/connector"
"google.golang.org/protobuf/types/known/emptypb"
) )
const ( const (
@@ -194,26 +192,3 @@ func randomMachineName() (string, error) {
} }
return "machine-" + suffix, nil return "machine-" + suffix, nil
} }
// WaitMachineReady waits for the machine API to respond.
func WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
cli, err := m.Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
}
defer cli.Close()
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(10*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
inspect := func() error {
if _, err := cli.Inspect(ctx, &emptypb.Empty{}); err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
return nil
}
return backoff.Retry(inspect, boff)
}
+71
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
) )
@@ -98,6 +99,76 @@ func (c *ConfigMount) Validate() error {
return nil return nil
} }
// Compare compares this ConfigMount with another.
// Returns:
//
// -1 if c < other
// 0 if c == other
// +1 if c > other
func (c *ConfigMount) Compare(other *ConfigMount) int {
if c.ConfigName != other.ConfigName {
if c.ConfigName < other.ConfigName {
return -1
}
return 1
}
if c.ContainerPath != other.ContainerPath {
if c.ContainerPath < other.ContainerPath {
return -1
}
return 1
}
if c.Uid != other.Uid {
if c.Uid < other.Uid {
return -1
}
return 1
}
if c.Gid != other.Gid {
if c.Gid < other.Gid {
return -1
}
return 1
}
// Compare Mode (handle nil cases)
if c.Mode == nil && other.Mode != nil {
return -1
}
if c.Mode != nil && other.Mode == nil {
return 1
}
if c.Mode != nil && other.Mode != nil {
if *c.Mode < *other.Mode {
return -1
}
if *c.Mode > *other.Mode {
return 1
}
}
return 0
}
// Equals compares two ConfigMount instances for equality.
func (c *ConfigMount) Equals(other *ConfigMount) bool {
return c.Compare(other) == 0
}
func (c *ConfigMount) Clone() ConfigMount {
clone := *c
if c.Mode != nil {
mode := *c.Mode
clone.Mode = &mode
}
return clone
}
// sortConfigMounts sorts a slice of ConfigMount instances.
func sortConfigMounts(mounts []ConfigMount) {
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Compare(&mounts[j]) < 0
})
}
// ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs // ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs
func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error { func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error {
configMap := make(map[string]struct{}) configMap := make(map[string]struct{})
+4
View File
@@ -135,6 +135,10 @@ func (c *Container) UnmarshalJSON(data []byte) error {
} }
*c = Container(temp) *c = Container(temp)
if c.ContainerJSONBase == nil {
return fmt.Errorf("container data is missing mandatory base fields: %s", data)
}
c.Name = strings.TrimPrefix(c.Name, "/") c.Name = strings.TrimPrefix(c.Name, "/")
return nil return nil
+38 -1
View File
@@ -225,6 +225,10 @@ func (s *ServiceSpec) Clone() ServiceSpec {
// ContainerSpec defines the desired state of a container in a service. // ContainerSpec defines the desired state of a container in a service.
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated. // ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
type ContainerSpec struct { type ContainerSpec struct {
// Specifies which additional capabilities should be added for the container.
CapAdd []string
// Specifies which capabilities should be dropped from the container.
CapDrop []string
// Command overrides the default CMD of the image to be executed when running a container. // Command overrides the default CMD of the image to be executed when running a container.
Command []string Command []string
// Entrypoint overrides the default ENTRYPOINT of the image. // Entrypoint overrides the default ENTRYPOINT of the image.
@@ -243,6 +247,8 @@ type ContainerSpec struct {
PullPolicy string PullPolicy string
// Resource allocation for the container. // Resource allocation for the container.
Resources ContainerResources Resources ContainerResources
// Namespaced kernel parameters to be set in container
Sysctls map[string]string
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID]. // User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
User string User string
// VolumeMounts specifies how volumes are mounted into the container filesystem. // VolumeMounts specifies how volumes are mounted into the container filesystem.
@@ -290,18 +296,32 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
orig := s.SetDefaults() orig := s.SetDefaults()
spec = spec.SetDefaults() spec = spec.SetDefaults()
// Volumes
slices.Sort(orig.Volumes) slices.Sort(orig.Volumes)
slices.Sort(spec.Volumes) slices.Sort(spec.Volumes)
// Volume mounts
sortVolumeMounts(orig.VolumeMounts) sortVolumeMounts(orig.VolumeMounts)
sortVolumeMounts(spec.VolumeMounts) sortVolumeMounts(spec.VolumeMounts)
// Config mounts
sortConfigMounts(orig.ConfigMounts)
sortConfigMounts(spec.ConfigMounts)
return cmp.Equal(orig, spec, cmpopts.EquateEmpty()) return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
} }
func (s *ContainerSpec) Clone() ContainerSpec { func (s *ContainerSpec) Clone() ContainerSpec {
spec := *s spec := *s
if s.CapAdd != nil {
spec.CapAdd = make([]string, len(s.CapAdd))
copy(spec.CapAdd, s.CapAdd)
}
if s.CapDrop != nil {
spec.CapDrop = make([]string, len(s.CapDrop))
copy(spec.CapDrop, s.CapDrop)
}
if s.Command != nil { if s.Command != nil {
spec.Command = make([]string, len(s.Command)) spec.Command = make([]string, len(s.Command))
copy(spec.Command, s.Command) copy(spec.Command, s.Command)
@@ -317,6 +337,12 @@ func (s *ContainerSpec) Clone() ContainerSpec {
} }
spec.LogDriver = &logDriver spec.LogDriver = &logDriver
} }
if s.Env != nil {
spec.Env = make(EnvVars, len(s.Env))
for k, v := range s.Env {
spec.Env[k] = v
}
}
if s.Volumes != nil { if s.Volumes != nil {
spec.Volumes = make([]string, len(s.Volumes)) spec.Volumes = make([]string, len(s.Volumes))
copy(spec.Volumes, s.Volumes) copy(spec.Volumes, s.Volumes)
@@ -325,7 +351,18 @@ func (s *ContainerSpec) Clone() ContainerSpec {
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts)) spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
copy(spec.VolumeMounts, s.VolumeMounts) copy(spec.VolumeMounts, s.VolumeMounts)
} }
if s.ConfigMounts != nil {
spec.ConfigMounts = make([]ConfigMount, len(s.ConfigMounts))
for i, cm := range s.ConfigMounts {
spec.ConfigMounts[i] = cm.Clone()
}
}
if s.Sysctls != nil {
spec.Sysctls = make(map[string]string, len(s.Sysctls))
for k, v := range s.Sysctls {
spec.Sysctls[k] = v
}
}
return spec return spec
} }
+94
View File
@@ -1,11 +1,19 @@
package api package api
import ( import (
"os"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// boolPtr is a convenience function to create a pointer to a uint64 value
// TODO: Make this a generic function that works for any type
func boolPtr(b bool) *bool {
return &b
}
func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -197,3 +205,89 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
}) })
} }
} }
func TestContainerSpec_Clone(t *testing.T) {
mode := os.FileMode(0o644)
original := ContainerSpec{
CapAdd: []string{"NET_ADMIN"},
CapDrop: []string{"ALL"},
Command: []string{"sh", "-c", "echo hello"},
Entrypoint: []string{"/bin/bash"},
Env: EnvVars{
"FOO": "bar",
"BAZ": "qux",
},
Image: "nginx:latest",
Init: boolPtr(true),
LogDriver: &LogDriver{
Name: "json-file",
Options: map[string]string{
"max-size": "10m",
},
},
Privileged: true,
PullPolicy: PullPolicyAlways,
Resources: ContainerResources{
CPU: 1234,
Memory: 2345,
MemoryReservation: 3456,
},
Sysctls: map[string]string{
"net.ipv4.ip_forward": "1",
},
User: "1000:1000",
Volumes: []string{"/data", "/config"},
VolumeMounts: []VolumeMount{
{VolumeName: "data", ContainerPath: "/data"},
},
ConfigMounts: []ConfigMount{
{ConfigName: "app-config", ContainerPath: "/etc/config", Mode: &mode},
},
}
cloned := original.Clone()
// Check ContainerSpec equality
assert.True(t, original.Equals(cloned))
// Verify deep copy by modifying the original
stringModified := "modified"
original.CapAdd[0] = stringModified
original.CapDrop[0] = stringModified
original.Command[0] = stringModified
original.Entrypoint[0] = stringModified
original.Env["FOO"] = stringModified
original.LogDriver.Options["max-size"] = stringModified
original.Volumes[0] = stringModified
original.VolumeMounts[0].ContainerPath = stringModified
original.ConfigMounts[0].ContainerPath = stringModified
*original.ConfigMounts[0].Mode = 0o755 // Modify the Mode pointer value
original.Sysctls["net.ipv4.ip_forward"] = stringModified
assert.False(t, original.Equals(cloned))
// Assert cloned values are unchanged
assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0])
assert.Equal(t, "ALL", cloned.CapDrop[0])
assert.Equal(t, "sh", cloned.Command[0])
assert.Equal(t, "/bin/bash", cloned.Entrypoint[0])
assert.Equal(t, "bar", cloned.Env["FOO"])
assert.Equal(t, "qux", cloned.Env["BAZ"])
assert.Equal(t, "nginx:latest", cloned.Image)
assert.NotNil(t, cloned.Init)
assert.Equal(t, true, *cloned.Init)
assert.NotNil(t, cloned.LogDriver)
assert.Equal(t, "json-file", cloned.LogDriver.Name)
assert.Equal(t, "10m", cloned.LogDriver.Options["max-size"])
assert.Equal(t, true, cloned.Privileged)
assert.Equal(t, PullPolicyAlways, cloned.PullPolicy)
assert.Equal(t, int64(1234), cloned.Resources.CPU)
assert.Equal(t, int64(2345), cloned.Resources.Memory)
assert.Equal(t, int64(3456), cloned.Resources.MemoryReservation)
assert.Equal(t, "1000:1000", cloned.User)
assert.Equal(t, "/data", cloned.Volumes[0])
assert.Equal(t, "/data", cloned.VolumeMounts[0].ContainerPath)
assert.Equal(t, "/etc/config", cloned.ConfigMounts[0].ContainerPath)
assert.NotNil(t, cloned.ConfigMounts[0].Mode)
assert.Equal(t, os.FileMode(0o644), *cloned.ConfigMounts[0].Mode, "Mode should be deep copied")
assert.Equal(t, "1", cloned.Sysctls["net.ipv4.ip_forward"])
}
+19 -6
View File
@@ -13,18 +13,27 @@ import (
func configSpecsFromCompose( func configSpecsFromCompose(
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string, configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
) ([]api.ConfigSpec, []api.ConfigMount, error) { ) ([]api.ConfigSpec, []api.ConfigMount, error) {
var configSpecs []api.ConfigSpec
var configMounts []api.ConfigMount var configMounts []api.ConfigMount
// Temporary map to hold config specs
configSpecsMap := make(map[string]api.ConfigSpec)
// We iterate over all service config objects (config mounts)
for _, serviceConfig := range serviceConfigs { for _, serviceConfig := range serviceConfigs {
var spec api.ConfigSpec var spec api.ConfigSpec
if projectConfig, exists := configs[serviceConfig.Source]; exists { projectConfig, exists := configs[serviceConfig.Source]
if !exists {
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
}
if projectConfig.External { if projectConfig.External {
return nil, nil, fmt.Errorf("external configs are not supported: %s", return nil, nil, fmt.Errorf("external configs are not supported: %s",
serviceConfig.Source) serviceConfig.Source)
} }
spec, exists = configSpecsMap[serviceConfig.Source]
if !exists {
spec = api.ConfigSpec{ spec = api.ConfigSpec{
Name: serviceConfig.Source, Name: serviceConfig.Source,
Content: []byte(projectConfig.Content), Content: []byte(projectConfig.Content),
@@ -44,11 +53,9 @@ func configSpecsFromCompose(
} }
spec.Content = fileContent spec.Content = fileContent
} }
} else {
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
}
configSpecs = append(configSpecs, spec) configSpecsMap[serviceConfig.Source] = spec
}
// Create config mount // Create config mount
target := serviceConfig.Target target := serviceConfig.Target
@@ -71,5 +78,11 @@ func configSpecsFromCompose(
configMounts = append(configMounts, mount) configMounts = append(configMounts, mount)
} }
var configSpecs []api.ConfigSpec
// Convert config spec map to slice
for _, spec := range configSpecsMap {
configSpecs = append(configSpecs, spec)
}
return configSpecs, configMounts, nil return configSpecs, configMounts, nil
} }
+141
View File
@@ -77,6 +77,147 @@ func TestConfigSpecsFromCompose(t *testing.T) {
}, },
}, },
}, },
{
name: "same source mounted to different targets",
configs: types.Configs{
"shared-config": types.ConfigObjConfig{
File: "testdata/config1.txt",
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "shared-config",
Target: "/app/config.json",
UID: "1000",
GID: "1000",
},
{
Source: "shared-config",
Target: "/backup/config.json",
UID: "1001",
GID: "1001",
},
},
expectedSpecs: []api.ConfigSpec{
{
Name: "shared-config",
Content: []byte("test config content\n"),
},
},
expectedMounts: []api.ConfigMount{
{
ConfigName: "shared-config",
ContainerPath: "/app/config.json",
Uid: "1000",
Gid: "1000",
},
{
ConfigName: "shared-config",
ContainerPath: "/backup/config.json",
Uid: "1001",
Gid: "1001",
},
},
},
{
name: "config with default target path",
configs: types.Configs{
"default-config": types.ConfigObjConfig{
Content: "inline config content",
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "default-config",
// No Target specified - should use default
},
},
expectedSpecs: []api.ConfigSpec{
{
Name: "default-config",
Content: []byte("inline config content"),
},
},
expectedMounts: []api.ConfigMount{
{
ConfigName: "default-config",
ContainerPath: "/default-config",
},
},
},
{
name: "config with inline content",
configs: types.Configs{
"inline-config": types.ConfigObjConfig{
Content: "server {\n listen 80;\n}",
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "inline-config",
Target: "/etc/nginx/sites-available/default",
Mode: func() *types.FileMode { m := types.FileMode(0o755); return &m }(),
},
},
expectedSpecs: []api.ConfigSpec{
{
Name: "inline-config",
Content: []byte("server {\n listen 80;\n}"),
},
},
expectedMounts: []api.ConfigMount{
{
ConfigName: "inline-config",
ContainerPath: "/etc/nginx/sites-available/default",
Mode: func() *os.FileMode { m := os.FileMode(0o755); return &m }(),
},
},
},
{
name: "config not found error",
configs: types.Configs{
"existing-config": types.ConfigObjConfig{
Content: "some content",
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "missing-config",
Target: "/app/config.json",
},
},
expectError: true,
},
{
name: "external config error",
configs: types.Configs{
"external-config": types.ConfigObjConfig{
External: true,
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "external-config",
Target: "/app/config.json",
},
},
expectError: true,
},
{
name: "file not found error",
configs: types.Configs{
"missing-file-config": types.ConfigObjConfig{
File: "testdata/nonexistent.txt",
},
},
serviceConfigs: []types.ServiceConfigObjConfig{
{
Source: "missing-file-config",
Target: "/app/config.json",
},
},
expectError: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
+3
View File
@@ -43,6 +43,8 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Container: api.ContainerSpec{ Container: api.ContainerSpec{
CapAdd: service.CapAdd,
CapDrop: service.CapDrop,
Command: service.Command, Command: service.Command,
Entrypoint: service.Entrypoint, Entrypoint: service.Entrypoint,
Env: env, Env: env,
@@ -51,6 +53,7 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
Privileged: service.Privileged, Privileged: service.Privileged,
PullPolicy: pullPolicy, PullPolicy: pullPolicy,
Resources: resourcesFromCompose(service), Resources: resourcesFromCompose(service),
Sysctls: service.Sysctls,
User: service.User, User: service.User,
}, },
Name: serviceName, Name: serviceName,
+5
View File
@@ -100,6 +100,8 @@ func TestServiceSpecFromCompose(t *testing.T) {
Name: "test", Name: "test",
Mode: api.ServiceModeReplicated, Mode: api.ServiceModeReplicated,
Container: api.ContainerSpec{ Container: api.ContainerSpec{
CapAdd: []string{"NET_ADMIN"},
CapDrop: []string{"ALL"},
Command: []string{"nginx", "updated", "command"}, Command: []string{"nginx", "updated", "command"},
Entrypoint: []string{"/updated-docker-entrypoint.sh"}, Entrypoint: []string{"/updated-docker-entrypoint.sh"},
Env: map[string]string{ Env: map[string]string{
@@ -123,6 +125,9 @@ func TestServiceSpecFromCompose(t *testing.T) {
Memory: 100 * units.MiB, Memory: 100 * units.MiB,
MemoryReservation: 50 * units.MiB, MemoryReservation: 50 * units.MiB,
}, },
Sysctls: map[string]string{
"net.ipv4.ip_forward": "1",
},
User: "nginx:nginx", User: "nginx:nginx",
VolumeMounts: []api.VolumeMount{ VolumeMounts: []api.VolumeMount{
{ {
+6
View File
@@ -1,5 +1,9 @@
services: services:
test: test:
cap_add:
- NET_ADMIN
cap_drop:
- ALL
command: ["nginx", "updated", "command"] command: ["nginx", "updated", "command"]
cpus: 0.5 cpus: 0.5
entrypoint: ["/updated-docker-entrypoint.sh"] entrypoint: ["/updated-docker-entrypoint.sh"]
@@ -19,6 +23,8 @@ services:
privileged: true privileged: true
pull_policy: always pull_policy: always
scale: 3 scale: 3
sysctls:
- net.ipv4.ip_forward=1
user: nginx:nginx user: nginx:nginx
volumes: volumes:
- /etc/passwd:/host/etc/passwd:ro - /etc/passwd:/host/etc/passwd:ro
+30
View File
@@ -0,0 +1,30 @@
package connector
import (
"encoding/json"
"fmt"
)
// defaultServiceConfig defines the default gRPC service configuration including retry policy for transient failures.
var defaultServiceConfig = mustMarshalJSON(map[string]any{
"methodConfig": []map[string]any{
{
"name": []map[string]string{{"service": ""}},
"retryPolicy": map[string]any{
"maxAttempts": 5, // 5 is the maximum allowed by gRPC
"initialBackoff": "0.5s",
"maxBackoff": "5s",
"backoffMultiplier": 2,
"retryableStatusCodes": []string{"UNAVAILABLE"},
},
},
},
})
func mustMarshalJSON(v any) string {
b, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("failed to marshal service config: %v", err))
}
return string(b)
}
+1
View File
@@ -59,6 +59,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
conn, err := grpc.NewClient( conn, err := grpc.NewClient(
"unix://"+sockPath, "unix://"+sockPath,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
grpc.WithContextDialer( grpc.WithContextDialer(
func(ctx context.Context, addr string) (net.Conn, error) { func(ctx context.Context, addr string) (net.Conn, error) {
addr = strings.TrimPrefix(addr, "unix://") addr = strings.TrimPrefix(addr, "unix://")
+1
View File
@@ -90,6 +90,7 @@ func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error)
grpcConn, err := grpc.NewClient( grpcConn, err := grpc.NewClient(
"passthrough:///", // Dummy target since we're using a custom dialer. "passthrough:///", // Dummy target since we're using a custom dialer.
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return c.conn, nil return c.conn, nil
}), }),
+1
View File
@@ -23,6 +23,7 @@ func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
conn, err := grpc.NewClient( conn, err := grpc.NewClient(
c.apiAddr.String(), c.apiAddr.String(),
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("create machine API client: %w", err) return nil, fmt.Errorf("create machine API client: %w", err)
+1
View File
@@ -25,6 +25,7 @@ func (c *UnixConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
conn, err := grpc.NewClient( conn, err := grpc.NewClient(
target, target,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("create machine API client: %w", err) return nil, fmt.Errorf("create machine API client: %w", err)
+1
View File
@@ -66,6 +66,7 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err
conn, err := grpc.NewClient( conn, err := grpc.NewClient(
machineAPIAddr, machineAPIAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return c.tun.DialContext(ctx, "tcp", addr) return c.tun.DialContext(ctx, "tcp", addr)
}), }),
+59
View File
@@ -9,6 +9,44 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestEvalContainerSpecChange_ContainerCapAdd(t *testing.T) {
t.Parallel()
currentSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
}
newSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
CapAdd: []string{"NET_ADMIN"},
},
}
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec))
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
}
func TestEvalContainerSpecChange_ContainerCapDrop(t *testing.T) {
t.Parallel()
currentSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
}
newSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
CapDrop: []string{"ALL"},
},
}
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec))
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
}
func TestEvalContainerSpecChange_ContainerResources(t *testing.T) { func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
t.Parallel() t.Parallel()
@@ -325,6 +363,27 @@ func TestEvalContainerSpecChange_ContainerPrivileged(t *testing.T) {
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
} }
func TestEvalContainerSpecChange_ContainerSysctls(t *testing.T) {
t.Parallel()
currentSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
}
newSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
Sysctls: map[string]string{
"net.ipv4.ip_forward": "1",
},
},
}
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec))
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
}
func TestEvalContainerSpecChange_PullPolicy(t *testing.T) { func TestEvalContainerSpecChange_PullPolicy(t *testing.T) {
t.Parallel() t.Parallel()
+2 -5
View File
@@ -61,14 +61,11 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) (
continue continue
} }
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
if err = verifyCaddyReachable(ctx, m.Machine); err == nil { if err = verifyCaddyReachable(ctx, m.Machine); err == nil {
reachableMachines <- m.Machine reachableMachines <- m.Machine
} }
}() })
} }
go func() { go func() {
+48
View File
@@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -13,6 +15,7 @@ import (
) )
func (cli *Client) InspectMachine(ctx context.Context, nameOrID string) (*pb.MachineMember, error) { func (cli *Client) InspectMachine(ctx context.Context, nameOrID string) (*pb.MachineMember, error) {
// TODO: refactor to use MachineClient.InspectMachine.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -99,3 +102,48 @@ func (cli *Client) RenameMachine(ctx context.Context, nameOrID, newName string)
return cli.UpdateMachine(ctx, req) return cli.UpdateMachine(ctx, req)
} }
// WaitMachineReady waits for the machine API on the connected machine to respond.
func (cli *Client) WaitMachineReady(ctx context.Context, timeout time.Duration) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
inspect := func() error {
if _, err := cli.Inspect(ctx, &emptypb.Empty{}); err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
return nil
}
return backoff.Retry(inspect, boff)
}
// WaitClusterReady waits for the connected machine to be ready to server cluster requests.
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
// doing retries with backoff for Unavailable errors. However, it's still convenient to use backoff
// to control the overall timeout for the operation.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
listMachines := func() error {
_, err := cli.ListMachines(ctx, nil)
if err != nil {
if s, ok := status.FromError(err); ok &&
// TODO: remove FailedPrecondition after releading 0.17.
(s.Code() == codes.Unavailable || s.Code() == codes.FailedPrecondition) {
// Machine is not ready yet, retry.
return err
}
// Other non-Unavailable errors should not be retried.
return backoff.Permanent(err)
}
return nil
}
return backoff.Retry(listMachines, boff)
}
+6 -18
View File
@@ -240,11 +240,7 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
// Remove all containers on all machines that belong to the service. // Remove all containers on all machines that belong to the service.
for _, mc := range svc.Containers { for _, mc := range svc.Containers {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, container.StopOptions{}) err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, container.StopOptions{})
if err != nil { if err != nil {
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err) errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
@@ -258,7 +254,7 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
if err != nil && !errors.Is(err, api.ErrNotFound) { if err != nil && !errors.Is(err, api.ErrNotFound) {
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err) errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
} }
}() })
} }
go func() { go func() {
@@ -286,16 +282,12 @@ func (cli *Client) StopService(ctx context.Context, id string, opts container.St
// Stop all containers on all machines that belong to the service. // Stop all containers on all machines that belong to the service.
for _, mc := range svc.Containers { for _, mc := range svc.Containers {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, opts) err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, opts)
if err != nil { if err != nil {
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err) errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
} }
}() })
} }
go func() { go func() {
@@ -323,16 +315,12 @@ func (cli *Client) StartService(ctx context.Context, id string) error {
// Start all containers on all machines that belong to the service. // Start all containers on all machines that belong to the service.
for _, mc := range svc.Containers { for _, mc := range svc.Containers {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
err := cli.StartContainer(ctx, svc.ID, mc.Container.ID) err := cli.StartContainer(ctx, svc.ID, mc.Container.ID)
if err != nil { if err != nil {
errCh <- fmt.Errorf("start container '%s': %w", mc.Container.ID, err) errCh <- fmt.Errorf("start container '%s': %w", mc.Container.ID, err)
} }
}() })
} }
go func() { go func() {
+4 -6
View File
@@ -55,7 +55,7 @@ func createTestCluster(
}) })
if waitReady { if waitReady {
require.NoError(t, p.WaitClusterReady(ctx, c, 15*time.Second)) require.NoError(t, p.WaitClusterReady(ctx, c, 30*time.Second))
} }
return c, p return c, p
@@ -73,7 +73,6 @@ func TestClusterLifecycle(t *testing.T) {
// Create a client for each machine and wait for it to be ready. // Create a client for each machine and wait for it to be ready.
clients := make([]*client.Client, len(c.Machines)) clients := make([]*client.Client, len(c.Machines))
for i, m := range c.Machines { for i, m := range c.Machines {
require.NoError(t, ucind.WaitMachineReady(ctx, m, 5*time.Second))
clients[i], err = m.Connect(ctx) clients[i], err = m.Connect(ctx)
require.NoError(t, err) require.NoError(t, err)
//goland:noinspection GoDeferInLoop //goland:noinspection GoDeferInLoop
@@ -86,12 +85,11 @@ func TestClusterLifecycle(t *testing.T) {
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
if err != nil { if err != nil {
// FailedPrecondition "cluster is not initialised" is expected until the store is reconciled. // Unavailable "machine is not ready to serve cluster requests" is expected until
if s, ok := status.FromError(err); ok { // the store is reconciled.
if s.Code() == codes.FailedPrecondition { if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable {
return false return false
} }
}
require.NoError(t, err) require.NoError(t, err)
} }
+14
View File
@@ -66,6 +66,11 @@ func TestComposeConfigs(t *testing.T) {
Gid: "1000", Gid: "1000",
Mode: func() *os.FileMode { m := os.FileMode(0o600); return &m }(), Mode: func() *os.FileMode { m := os.FileMode(0o600); return &m }(),
}, },
{
ConfigName: "from-file",
ContainerPath: "/etc/new-dir/config-from-file.conf",
Mode: func() *os.FileMode { m := os.FileMode(0o644); return &m }(),
},
}, },
}, },
Configs: []api.ConfigSpec{ Configs: []api.ConfigSpec{
@@ -110,5 +115,14 @@ func TestComposeConfigs(t *testing.T) {
userId: 1000, userId: 1000,
groupId: 1000, groupId: 1000,
}, configContentSecond) }, configContentSecond)
configContentThird, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/new-dir/config-from-file.conf")
require.NoError(t, err)
assert.Equal(t, fileInfo{
permissions: 0o644,
content: "this is file config\n",
userId: 0,
groupId: 0,
}, configContentThird, "Same config should be mountable to multiple paths, including nested directories")
}) })
} }
+4
View File
@@ -11,6 +11,10 @@ services:
uid: "1000" uid: "1000"
gid: "1000" gid: "1000"
mode: 0600 mode: 0600
# Writing a config to a new, non-existent directory
- source: from-file
target: /etc/new-dir/config-from-file.conf
mode: 0644
deploy: deploy:
replicas: 1 replicas: 1
configs: configs:
-1
View File
@@ -71,7 +71,6 @@ func readFileInfoInContainer(t *testing.T, cli *client.Client, serviceNameOrID,
// Parse permissions // Parse permissions
permissions := strings.TrimSpace(permOutput) permissions := strings.TrimSpace(permOutput)
fmt.Printf("Permissions output: %s\n", permissions)
// Parse three numbers: permissions, uid, gid // Parse three numbers: permissions, uid, gid
var permissionsOctal, uid, gid int var permissionsOctal, uid, gid int
+7 -3
View File
@@ -293,7 +293,7 @@ func TestDeployment(t *testing.T) {
config, err := cli.Caddy.GetConfig(ctx, nil) config, err := cli.Caddy.GetConfig(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, config.Caddyfile, "# This file is autogenerated by Uncloud") assert.Contains(t, config.Caddyfile, "# Caddyfile autogenerated by Uncloud")
assert.Contains(t, config.Caddyfile, "handle /.uncloud-verify") assert.Contains(t, config.Caddyfile, "handle /.uncloud-verify")
}) })
@@ -423,7 +423,7 @@ myapp.example.com {
}, 5*time.Second, 100*time.Millisecond, }, 5*time.Second, 100*time.Millisecond,
"Expected both custom configs to be included in the Caddyfile") "Expected both custom configs to be included in the Caddyfile")
assert.Contains(t, config.Caddyfile, "# This file is autogenerated by Uncloud") assert.Contains(t, config.Caddyfile, "# Caddyfile autogenerated by Uncloud")
assert.Contains(t, config.Caddyfile, "handle /.uncloud-verify") assert.Contains(t, config.Caddyfile, "handle /.uncloud-verify")
assert.Contains(t, config.Caddyfile, caddyCaddyfile, assert.Contains(t, config.Caddyfile, caddyCaddyfile,
"Expected user-defined global Caddy config to be included in the Caddyfile") "Expected user-defined global Caddy config to be included in the Caddyfile")
@@ -480,7 +480,11 @@ myapp.example.com {
// Check that the Caddy config hasn't changed. // Check that the Caddy config hasn't changed.
newConfig, err := cli.Caddy.GetConfig(ctx, nil) newConfig, err := cli.Caddy.GetConfig(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, validConfig, newConfig.Caddyfile,
// Compare stable parts of the Caddyfile only (skip autogenerated comment with timestamp).
_, stableValidConfig, _ := strings.Cut(validConfig, "\n")
_, stableNewConfig, _ := strings.Cut(newConfig.Caddyfile, "\n")
assert.Equal(t, stableValidConfig, stableNewConfig,
"Caddy config should not change when an invalid user-defined Caddy config is deployed") "Caddy config should not change when an invalid user-defined Caddy config is deployed")
}) })
@@ -4,6 +4,14 @@ In this guide, we'll deploy [Excalidraw](https://excalidraw.com) — a popular s
Linux server. You'll learn the **basics of Uncloud** and see how simple it is to **run web apps** on your own Linux server. You'll learn the **basics of Uncloud** and see how simple it is to **run web apps** on your own
infrastructure with secure internet access. infrastructure with secure internet access.
:::info NOTE
To give you a chance to play with Uncloud without even leaving your browser or needing your own servers, we're providing interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
You can follow [this tutorial](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) which walks you through creating a new cluster with two machines and then deploying a simple web service to it.
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you can play with an already initialized Uncloud cluster.
:::
## Prerequisites ## Prerequisites
Before you begin, you'll need: Before you begin, you'll need:
@@ -10,7 +10,7 @@ with [Let's Encrypt](https://letsencrypt.org/), and route requests to your servi
## How it works ## How it works
By default, Caddy runs as a global service `caddy` on every machine in your cluster, listening on the host ports 80 By default, Caddy runs as a global service `caddy` on every machine in your cluster, listening on the host ports 80
(HTTP) and 443 (HTTPS). (HTTP), 443 (HTTPS), and 443/UDP (HTTP/3).
It's deployed during cluster initialisation (`uc machine init`) unless you use the `--no-caddy` flag. It's deployed during cluster initialisation (`uc machine init`) unless you use the `--no-caddy` flag.
See [Managing Caddy](3-managing-caddy.md) for deployment and customisation instructions. See [Managing Caddy](3-managing-caddy.md) for deployment and customisation instructions.
@@ -197,8 +197,9 @@ debugging and verifying your `x-caddy` configs.
Example output: Example output:
```caddyfile ```caddyfile
# This file is autogenerated by Uncloud based on the configuration of running services. # Caddyfile autogenerated by Uncloud (DO NOT EDIT): 2025-12-20T22:43:56Z
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'. # User-defined global config from service 'caddy'.
*.example.com { *.example.com {
@@ -94,6 +94,7 @@ services:
x-ports: x-ports:
- 80:80@host - 80:80@host
- 443:443@host - 443:443@host
- 443:443/udp@host
x-caddy: Caddyfile x-caddy: Caddyfile
deploy: deploy:
mode: global mode: global
@@ -138,7 +139,8 @@ internal.example.com {
:::info note :::info note
The specified `command`, `environment`, `volumes`, and `x-ports` properties are essential for Caddy to function The specified `command`, `environment`, `volumes`, and `x-ports` properties are essential for Caddy to function
correctly in the Uncloud cluster. correctly in the Uncloud cluster. Do not change the source paths of the volume mounts as the Uncloud daemon relies on
them to communicate with Caddy and update its configuration.
::: :::
@@ -160,8 +162,9 @@ uc caddy config
Example output: Example output:
```caddyfile ```caddyfile
# This file is autogenerated by Uncloud based on the configuration of running services. # Caddyfile autogenerated by Uncloud (DO NOT EDIT): 2025-12-22T10:30:12Z
# Do not edit manually. Any manual changes will be overwritten on the next update. # Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'. # User-defined global config from service 'caddy'.
# Global options. # Global options.
@@ -4,9 +4,11 @@ Uncloud supports a subset of the [Compose specification](https://compose-spec.io
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` | ⚠️ Limited | 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_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 |
@@ -33,6 +35,7 @@ The following table shows the support status for main Compose features:
| `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 | | | `storage_opt` | ❌ Not supported | |
| `sysctls` | ✅ Supported | Namespaced kernel parameters |
| `user` | ✅ Supported | Set container user | | `user` | ✅ Supported | Set container user |
| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | | `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs |
| **Deploy** | | | | **Deploy** | | |
+5 -2
View File
@@ -19,17 +19,20 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume
* [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts. * [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts.
* [uc deploy](uc_deploy.md) - Deploy services from a Compose file. * [uc deploy](uc_deploy.md) - Deploy services from a Compose file.
* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. * [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS.
* [uc exec](uc_exec.md) - Execute a command in a running service container * [uc exec](uc_exec.md) - Execute a command in a running service container.
* [uc image](uc_image.md) - Manage images on machines in the cluster. * [uc image](uc_image.md) - Manage images on machines in the cluster.
* [uc images](uc_images.md) - List images on machines in the cluster. * [uc images](uc_images.md) - List images on machines in the cluster.
* [uc inspect](uc_inspect.md) - Display detailed information on a service. * [uc inspect](uc_inspect.md) - Display detailed information on a service.
* [uc logs](uc_logs.md) - View service logs. * [uc logs](uc_logs.md) - View service logs.
* [uc ls](uc_ls.md) - List services. * [uc ls](uc_ls.md) - List services.
* [uc machine](uc_machine.md) - Manage machines in the cluster. * [uc machine](uc_machine.md) - Manage machines in the cluster.
* [uc ps](uc_ps.md) - List all service containers in the cluster * [uc ps](uc_ps.md) - List all service containers.
* [uc rm](uc_rm.md) - Remove one or more services. * [uc rm](uc_rm.md) - Remove one or more services.
* [uc run](uc_run.md) - Run a service. * [uc run](uc_run.md) - Run a service.
* [uc scale](uc_scale.md) - Scale a replicated service by changing the number of replicas. * [uc scale](uc_scale.md) - Scale a replicated service by changing the number of replicas.
* [uc service](uc_service.md) - Manage services in the cluster. * [uc service](uc_service.md) - Manage services in the cluster.
* [uc start](uc_start.md) - Start one or more services.
* [uc stop](uc_stop.md) - Stop one or more services.
* [uc volume](uc_volume.md) - Manage volumes in the cluster. * [uc volume](uc_volume.md) - Manage volumes in the cluster.
* [uc wg](uc_wg.md) - Inspect WireGuard network
+1 -1
View File
@@ -1,6 +1,6 @@
# uc exec # uc exec
Execute a command in a running service container Execute a command in a running service container.
## Synopsis ## Synopsis
@@ -24,6 +24,8 @@ uc machine add [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")
-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
@@ -12,7 +12,7 @@ Connection methods:
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config) ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)
``` ```
uc machine init [USER@HOST:PORT] [flags] uc machine init [schema://]USER@HOST[:PORT] [flags]
``` ```
## Examples ## Examples
@@ -46,6 +46,8 @@ uc machine init [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")
-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
+2 -2
View File
@@ -1,6 +1,6 @@
# uc ps # uc ps
List all service containers in the cluster List all service containers.
## Synopsis ## Synopsis
@@ -17,7 +17,7 @@ uc ps [flags]
``` ```
-h, --help help for ps -h, --help help for ps
-s, --sort string Sort containers by 'service', 'machine' or 'health' (default "service") -s, --sort string Sort containers by 'service', 'machine', or 'health'. (default "service")
``` ```
## Options inherited from parent commands ## Options inherited from parent commands
+1 -1
View File
@@ -20,7 +20,7 @@ Manage services in the cluster.
## See also ## See also
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes. * [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc service exec](uc_service_exec.md) - Execute a command in a running service container * [uc service exec](uc_service_exec.md) - Execute a command in a running service container.
* [uc service inspect](uc_service_inspect.md) - Display detailed information on a service. * [uc service inspect](uc_service_inspect.md) - Display detailed information on a service.
* [uc service logs](uc_service_logs.md) - View service logs. * [uc service logs](uc_service_logs.md) - View service logs.
* [uc service ls](uc_service_ls.md) - List services. * [uc service ls](uc_service_ls.md) - List services.
@@ -1,6 +1,6 @@
# uc service exec # uc service exec
Execute a command in a running service container Execute a command in a running service container.
## Synopsis ## Synopsis
@@ -4,7 +4,10 @@ Start one or more services.
## Synopsis ## Synopsis
Start one or more services. Start one or more previously stopped services.
Starts all containers of the specified service(s) across all machines in the cluster.
Services can be specified by name or ID.
``` ```
uc service start SERVICE [SERVICE...] [flags] uc service start SERVICE [SERVICE...] [flags]
@@ -4,7 +4,10 @@ Stop one or more services.
## Synopsis ## Synopsis
Stop one or more services. Stop one or more running services.
Gracefully stops all containers of the specified service(s) across all machines in the cluster.
Services can be specified by name or ID. Stopped services can be restarted with 'uc start'.
``` ```
uc service stop SERVICE [SERVICE...] [flags] uc service stop SERVICE [SERVICE...] [flags]
@@ -14,8 +17,10 @@ uc service stop SERVICE [SERVICE...] [flags]
``` ```
-h, --help help for stop -h, --help help for stop
-s, --signal string Signal to send to the container -s, --signal string Signal to send to each container's main process.
-t, --timeout int Seconds to wait before killing the container Can be a signal name (SIGTERM, SIGINT, SIGHUP, etc.) or a number. (default SIGTERM)
-t, --timeout int Seconds to wait for each container to stop gracefully before forcibly killing it with SIGKILL.
Use -1 to wait indefinitely. (default 10)
``` ```
## Options inherited from parent commands ## Options inherited from parent commands
+34
View File
@@ -0,0 +1,34 @@
# uc start
Start one or more services.
## Synopsis
Start one or more previously stopped services.
Starts all containers of the specified service(s) across all machines in the cluster.
Services can be specified by name or ID.
```
uc start SERVICE [SERVICE...] [flags]
```
## Options
```
-h, --help help for start
```
## 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](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
+38
View File
@@ -0,0 +1,38 @@
# uc stop
Stop one or more services.
## Synopsis
Stop one or more running services.
Gracefully stops all containers of the specified service(s) across all machines in the cluster.
Services can be specified by name or ID. Stopped services can be restarted with 'uc start'.
```
uc stop SERVICE [SERVICE...] [flags]
```
## Options
```
-h, --help help for stop
-s, --signal string Signal to send to each container's main process.
Can be a signal name (SIGTERM, SIGINT, SIGHUP, etc.) or a number. (default SIGTERM)
-t, --timeout int Seconds to wait for each container to stop gracefully before forcibly killing it with SIGKILL.
Use -1 to wait indefinitely. (default 10)
```
## 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](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
+24
View File
@@ -0,0 +1,24 @@
# uc wg
Inspect WireGuard network
## Options
```
-h, --help help for wg
```
## 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](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc wg show](uc_wg_show.md) - Show WireGuard network configuration for a machine.
@@ -0,0 +1,32 @@
# uc wg show
Show WireGuard network configuration for a machine.
## Synopsis
Show the WireGuard network configuration for the machine currently connected to (or specified by the global --connect flag).
```
uc wg show [flags]
```
## Options
```
-h, --help help for show
-m, --machine string Name or ID of the machine to show the configuration for. (default is connected machine)
```
## 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 wg](uc_wg.md) - Inspect WireGuard network