Compare commits

..
1 Commits
Author SHA1 Message Date
Pasha Sviderski e698f6d06e draft hub landing 2026-05-15 17:56:47 +10:00
196 changed files with 4718 additions and 8705 deletions
+10 -19
View File
@@ -20,9 +20,9 @@ snapshot:
version_template: "{{ incminor .Version }}-nightly-{{.ShortCommit}}" version_template: "{{ incminor .Version }}-nightly-{{.ShortCommit}}"
builds: builds:
- id: uc - id: uncloud
main: ./cmd/uc main: ./cmd/uncloud
binary: uc binary: uncloud
env: env:
# Use CGO to be able to build fsevent required by compose on macOS. # Use CGO to be able to build fsevent required by compose on macOS.
- CGO_ENABLED={{ if eq .Os "darwin" }}1{{ else }}0{{ end }} - CGO_ENABLED={{ if eq .Os "darwin" }}1{{ else }}0{{ end }}
@@ -34,12 +34,7 @@ builds:
- amd64 - amd64
- arm64 - arm64
ldflags: ldflags:
- -s -w - -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
-X github.com/psviderski/uncloud/internal/version.version={{.Version}}
-X github.com/psviderski/uncloud/internal/version.commit={{.Commit}}
-X github.com/psviderski/uncloud/internal/version.dirty={{.IsGitDirty}}
-X github.com/psviderski/uncloud/internal/version.date={{.CommitDate}}
-X github.com/psviderski/uncloud/internal/version.builtBy=goreleaser
- id: uncloudd - id: uncloudd
main: ./cmd/uncloudd main: ./cmd/uncloudd
@@ -52,17 +47,12 @@ builds:
- amd64 - amd64
- arm64 - arm64
ldflags: ldflags:
- -s -w - -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
-X github.com/psviderski/uncloud/internal/version.version={{.Version}}
-X github.com/psviderski/uncloud/internal/version.commit={{.Commit}}
-X github.com/psviderski/uncloud/internal/version.dirty={{.IsGitDirty}}
-X github.com/psviderski/uncloud/internal/version.date={{.CommitDate}}
-X github.com/psviderski/uncloud/internal/version.builtBy=goreleaser
archives: archives:
- id: uc - id: uncloud
builds: builds:
- uc - uncloud
formats: formats:
# gz is not compatible with homebrew releases, so we use tar.gz # gz is not compatible with homebrew releases, so we use tar.gz
- tar.gz - tar.gz
@@ -103,9 +93,10 @@ brews:
description: "Uncloud CLI" description: "Uncloud CLI"
homepage: "https://uncloud.run" homepage: "https://uncloud.run"
ids: ids:
- uc - uncloud
install: | install: |
bin.install "uc" bin.install "uncloud"
bin.install_symlink "uncloud" => "uc"
skip_upload: false skip_upload: false
repository: repository:
owner: psviderski owner: psviderski
+3 -3
View File
@@ -72,7 +72,7 @@ management.
- **`cmd/`**: Contains main applications - **`cmd/`**: Contains main applications
- `uc/`: CLI tool with subcommands for machine, service, volume management - `uncloud/`: CLI tool with subcommands for machine, service, volume management
- `uncloudd/`: Daemon that runs on each machine - `uncloudd/`: Daemon that runs on each machine
- `ucind/`: Development cluster management for testing - `ucind/`: Development cluster management for testing
@@ -131,7 +131,7 @@ github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
```bash ```bash
# Build binaries # Build binaries
go build -o uc ./cmd/uc go build -o uncloud ./cmd/uncloud
go build -o uncloudd ./cmd/uncloudd go build -o uncloudd ./cmd/uncloudd
``` ```
@@ -239,7 +239,7 @@ uc context use <name> # Switch context
### Important Files to Understand ### Important Files to Understand
- `cmd/uc/main.go`: CLI entry point and command structure - `cmd/uncloud/main.go`: CLI entry point and command structure
- `internal/cli/cli.go`: CLI implementation and configuration - `internal/cli/cli.go`: CLI implementation and configuration
- `internal/machine/machine.go`: Core machine management - `internal/machine/machine.go`: Core machine management
- `pkg/api/`: Public API definitions - `pkg/api/`: Public API definitions
+5 -9
View File
@@ -18,7 +18,6 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd ./cmd/uncloudd
FROM alpine:${ALPINE_VERSION} AS corrosion-download FROM alpine:${ALPINE_VERSION} AS corrosion-download
ARG TARGETARCH ARG TARGETARCH
ARG CORROSION_VERSION
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \ RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
"amd64") echo "x86_64" ;; \ "amd64") echo "x86_64" ;; \
@@ -26,21 +25,20 @@ RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \ *) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
esac) \ esac) \
&& wget -q -O /tmp/corrosion.tar.gz \ && wget -q -O /tmp/corrosion.tar.gz \
"https://github.com/psviderski/corrosion/releases/download/v${CORROSION_VERSION}/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \ "https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \ && tar -xzf /tmp/corrosion.tar.gz -C /tmp \
&& install /tmp/corrosion /usr/bin/corrosion \ && install /tmp/corrosion /usr/local/bin/corrosion \
&& rm /tmp/corrosion.tar.gz /tmp/corrosion && rm /tmp/corrosion.tar.gz /tmp/corrosion
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features, # Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs. # e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
FROM gcr.io/distroless/cc-debian13:latest AS corrosion FROM gcr.io/distroless/cc-debian12:latest AS corrosion
COPY --from=corrosion-download /usr/bin/corrosion /usr/bin/corrosion COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
CMD ["corrosion", "agent"] CMD ["corrosion", "agent"]
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
ARG CORROSION_VERSION ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
ARG CORROSION_IMAGE="ghcr.io/unlabs-dev/corrosion:${CORROSION_VERSION}"
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@@ -49,8 +47,6 @@ RUN crane pull --platform ${TARGETOS}/${TARGETARCH} "${CORROSION_IMAGE}" /corros
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker. # Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
FROM docker:29.4.0-dind AS ucind FROM docker:29.4.0-dind AS ucind
ARG CORROSION_VERSION
# Create system group and user 'uncloud'. # Create system group and user 'uncloud'.
RUN addgroup -S uncloud && adduser -SHD -h /nonexistent -G uncloud -g "" uncloud RUN addgroup -S uncloud && adduser -SHD -h /nonexistent -G uncloud -g "" uncloud
RUN apk --no-cache add \ RUN apk --no-cache add \
+2 -2
View File
@@ -32,13 +32,13 @@ available outside the project, so they don't interfere with system packages.
Build the CLI: Build the CLI:
```shell ```shell
go build -o uc ./cmd/uc go build -o uc ./cmd/uncloud
``` ```
Or build and run the CLI with a single command: Or build and run the CLI with a single command:
```shell ```shell
go run ./cmd/uc --help go run ./cmd/uncloud --help
``` ```
The Uncloud daemon (`uncloudd`) only supports Linux, so you need to cross-compile it if you're developing on macOS or The Uncloud daemon (`uncloudd`) only supports Linux, so you need to cross-compile it if you're developing on macOS or
+4 -6
View File
@@ -1,7 +1,6 @@
# TODO: Makefile is deprecated, add new targets as mise tasks in mise.toml instead. # TODO: Makefile is deprecated, add new targets as mise tasks in mise.toml instead.
CORROSION_VERSION ?= 2026.6.15 CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
CORROSION_IMAGE = ghcr.io/unlabs-dev/corrosion:$(CORROSION_VERSION)
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
.PHONY: ucind-cluster .PHONY: ucind-cluster
@@ -10,12 +9,11 @@ ucind-cluster:
.PHONY: corrosion-image .PHONY: corrosion-image
corrosion-image: corrosion-image:
docker build --build-arg CORROSION_VERSION=$(CORROSION_VERSION) -t "$(CORROSION_IMAGE)" --target corrosion . docker build -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: corrosion-multiarch-image-push .PHONY: corrosion-multiarch-image-push
corrosion-multiarch-image-push: corrosion-multiarch-image-push:
docker buildx build --build-arg CORROSION_VERSION=$(CORROSION_VERSION) --push --platform linux/amd64,linux/arm64 \ docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
-t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: ucind-multiarch-image-push .PHONY: ucind-multiarch-image-push
ucind-multiarch-image-push: ucind-multiarch-image-push:
@@ -72,4 +70,4 @@ _lint:
.PHONY: cli-docs .PHONY: cli-docs
cli-docs: cli-docs:
go run ./cmd/uc docs go run ./cmd/uncloud docs
-4
View File
@@ -1,4 +0,0 @@
_ _ ___
| | | |/ __|
| |_| | (__
\__,_|\___|
-29
View File
@@ -1,29 +0,0 @@
package caddy
import (
"github.com/psviderski/uncloud/cmd/uc/service"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/logs"
"github.com/spf13/cobra"
)
func NewLogsCommand() *cobra.Command {
var options logs.Options
cmd := &cobra.Command{
Use: "logs",
Aliases: []string{"log"},
Short: "View caddy logs.",
Long: `View caddy logs.
This calls "uc logs caddy", see "uc logs" for the documention.
`,
RunE: func(cmd *cobra.Command, args []string) error {
args = append([]string{"caddy"}, args...)
uncli := cmd.Context().Value("cli").(*cli.CLI)
return service.RunLogs(cmd.Context(), uncli, args, options)
},
}
cmd.Flags().AddFlagSet(logs.Flags(&options))
return cmd
}
-103
View File
@@ -1,103 +0,0 @@
package context
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)
func TestCommandArgsValidation(t *testing.T) {
t.Parallel()
tests := []struct {
name string
newCmd func() *cobra.Command
args []string
wantErr bool
}{
{
name: "root accepts no args",
newCmd: NewRootCommand,
args: nil,
wantErr: false,
},
{
name: "root rejects extra args",
newCmd: NewRootCommand,
args: []string{"extra"},
wantErr: true,
},
{
name: "list accepts no args",
newCmd: NewListCommand,
args: nil,
wantErr: false,
},
{
name: "list rejects extra args",
newCmd: NewListCommand,
args: []string{"extra"},
wantErr: true,
},
{
name: "show accepts no args",
newCmd: NewShowCommand,
args: nil,
wantErr: false,
},
{
name: "show rejects extra args",
newCmd: NewShowCommand,
args: []string{"extra"},
wantErr: true,
},
{
name: "connection accepts no args",
newCmd: NewConnectionCommand,
args: nil,
wantErr: false,
},
{
name: "connection rejects extra args",
newCmd: NewConnectionCommand,
args: []string{"extra"},
wantErr: true,
},
{
name: "use accepts no args",
newCmd: NewUseCommand,
args: nil,
wantErr: false,
},
{
name: "use accepts one arg",
newCmd: NewUseCommand,
args: []string{"prod"},
wantErr: false,
},
{
name: "use rejects extra args",
newCmd: NewUseCommand,
args: []string{"prod", "extra"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
cmd := tt.newCmd()
require.NotNil(t, cmd.Args)
err := cmd.Args(cmd, tt.args)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
-176
View File
@@ -1,176 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/proxy"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type proxyOptions struct {
localPort int
remotePort int
service string
}
// NewProxyCommand creates a new command to proxy a local port to a service's port in the cluster.
func NewProxyCommand() *cobra.Command {
opts := proxyOptions{}
cmd := &cobra.Command{
Use: "proxy SERVICE [LOCAL_PORT:]REMOTE_PORT",
Args: cobra.ExactArgs(2),
Short: "Proxy a service port to a local port.",
Long: `Proxy a service port in the cluster to a local port on this machine.
If the service runs multiple containers, the command connects to the first running and healthy one.
If you don't provide a local port, the command picks a random one.
The connection stays open for as long as the command runs.`,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.service = args[0]
parts := strings.Split(args[1], ":")
switch len(parts) {
case 1:
remoteport, err := strconv.Atoi(parts[0])
if err != nil {
return fmt.Errorf("invalid remote port: '%s': %w", parts[0], err)
}
opts.remotePort = remoteport
case 2:
localport, err := strconv.Atoi(parts[0])
if err != nil {
return fmt.Errorf("invalid local port: '%s': %w", parts[0], err)
}
remoteport, err := strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("invalid remote port: '%s': %w", parts[1], err)
}
opts.localPort = localport
opts.remotePort = remoteport
default:
return fmt.Errorf("invalid port")
}
return runProxy(cmd.Context(), uncli, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
uncli := cmd.Context().Value("cli").(*cli.CLI)
return completion.Services(cmd.Context(), uncli, args, toComplete)
},
}
return cmd
}
func runProxy(ctx context.Context, uncli *cli.CLI, opts proxyOptions) error {
if opts.localPort < 0 || opts.localPort > 65535 {
return fmt.Errorf("invalid local port %d: must be between 0 and 65535", opts.localPort)
}
if opts.remotePort < 1 || opts.remotePort > 65535 {
return fmt.Errorf("invalid remote port %d: must be between 1 and 65535", opts.remotePort)
}
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
svc, err := clusterClient.InspectService(ctx, opts.service)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("service '%s' not found in the cluster", opts.service)
}
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
}
// Pick the first running and healthy container to proxy to.
var ctr *api.MachineServiceContainer
for i := range svc.Containers {
if svc.Containers[i].Container.Healthy() {
ctr = &svc.Containers[i]
break
}
}
if ctr == nil {
return fmt.Errorf("no running healthy container found for service '%s'", opts.service)
}
containerID := ctr.Container.ShortID()
ip := ctr.Container.UncloudNetworkIP()
if !ip.IsValid() {
return fmt.Errorf("container '%s' is not connected to the uncloud Docker network (could be host network)",
containerID)
}
dialer, err := clusterClient.Dialer()
if err != nil {
return fmt.Errorf("get proxy dialer: %w", err)
}
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(opts.localPort)))
if err != nil {
return fmt.Errorf("listen on 127.0.0.1:%d: %w", opts.localPort, err)
}
// There is no precheck if we can connect, as this always succeeds, only the proxy connects with the
// endpoint and shuffles the data, *it* will actually experience errors.
remoteAddr := net.JoinHostPort(ip.String(), strconv.Itoa(opts.remotePort))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
p := &proxy.Proxy{
Listener: listener,
RemoteAddr: remoteAddr,
DialContext: dialer.DialContext,
OnError: func(err error) {
fmt.Printf("Failed to proxy to '%s': %v\n", remoteAddr, err)
cancel()
},
}
// Run the proxy in the background and signal when it has fully shut down.
done := make(chan struct{})
go func() {
p.Run(ctx)
close(done)
}()
// Prefix the local address with the scheme for common HTTP ports so it becomes control-clickable in most
// terminals. We assume plain HTTP since TLS is typically terminated by Caddy in front of the service.
fmt.Printf("%s%s → %s (%s%s%s)\n", schemeForPort(opts.remotePort), p.Listener.Addr().String(),
remoteAddr, opts.service, tui.Faint.Render("/"), containerID)
<-ctx.Done()
// Wait for the proxy to drain in-flight connections and shut down gracefully.
<-done
return nil
}
// schemeForPort returns the "http://" URL scheme prefix for the ports most likely to serve plain HTTP,
// or an empty string otherwise.
func schemeForPort(port int) string {
switch port {
case 80, 3000, 8000, 8080, 8081, 8888, 9090:
return "http://"
default:
return ""
}
}
-105
View File
@@ -1,105 +0,0 @@
package main
import (
"context"
"encoding/json"
"testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
type mockDockerClient struct {
pb.DockerClient
listResp *pb.ListServiceContainersResponse
listErr error
}
func (m *mockDockerClient) ListServiceContainers(ctx context.Context, in *pb.ListServiceContainersRequest, opts ...grpc.CallOption) (*pb.ListServiceContainersResponse, error) {
return m.listResp, m.listErr
}
func TestCollectContainers(t *testing.T) {
containerData1 := map[string]interface{}{
"Id": "container1",
"Name": "container-1",
"Config": map[string]any{
"Image": "image-1",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON1, _ := json.Marshal(containerData1)
containerData2 := map[string]any{
"Id": "container2",
"Name": "container-2",
"Config": map[string]any{
"Image": "image-2",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON2, _ := json.Marshal(containerData2)
serviceSpecJSON, _ := json.Marshal(map[string]any{})
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: &pb.Metadata{MachineAddr: "10.0.0.1", MachineName: "machine-1"},
Containers: []*pb.ServiceContainer{
{
Container: containerJSON1,
ServiceSpec: serviceSpecJSON,
},
},
},
{
Metadata: &pb.Metadata{MachineAddr: "10.0.0.2", MachineName: "machine-2"},
Containers: []*pb.ServiceContainer{
{
Container: containerJSON2,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
cli := &client.Client{
Docker: &docker.Client{GRPCClient: mockDocker},
}
containers, err := collectContainers(context.Background(), cli)
require.NoError(t, err)
assert.Len(t, containers, 2)
for _, c := range containers {
if c.id == "container1" {
assert.Equal(t, "machine-1", c.machineName)
} else if c.id == "container2" {
assert.Equal(t, "machine-2", c.machineName)
} else {
t.Errorf("unexpected container id: %s", c.id)
}
}
}
-82
View File
@@ -1,82 +0,0 @@
package main
import (
"bytes"
_ "embed"
"fmt"
"strings"
"text/template"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
//go:embed art.txt
var asciiArt string
// NewVersionCommand creates a new command to print the version and build information for the binary.
func NewVersionCommand() *cobra.Command {
var output string
cmd := &cobra.Command{
Use: "version",
Short: "Show version and build information.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
info := version.GetInfo()
w := cmd.OutOrStdout()
switch output {
case "":
fmt.Fprint(w, humanVersion(info))
case "json":
s, err := info.JSONString()
if err != nil {
return err
}
fmt.Fprintln(w, s)
default:
s, err := templateVersion(output, info)
if err != nil {
return err
}
fmt.Fprint(w, s)
}
return nil
},
}
cmd.Flags().StringVarP(&output, "output", "o", "",
"Output format: 'json' or a Go template (e.g. '{{.Version}}').\n"+
"Run with '-o json' to discover the field names available to the template.\n"+
"(default is human-readable)")
return cmd
}
func humanVersion(info version.Info) string {
var b strings.Builder
b.WriteString(asciiArt)
b.WriteString("\n")
b.WriteString(fmt.Sprintf("uc: Uncloud CLI tool for deploying apps and managing resources (%s)",
tui.URLStyle.Render(version.WebsiteURL)))
b.WriteString("\n\n")
b.WriteString(info.String())
return b.String()
}
// templateVersion renders the version info using the provided Go template.
func templateVersion(tmpl string, info version.Info) (string, error) {
t, err := template.New("version").Parse(tmpl)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err = t.Execute(&buf, info); err != nil {
return "", fmt.Errorf("execute template: %w", err)
}
return buf.String(), nil
}
+3 -4
View File
@@ -10,10 +10,9 @@ import (
func NewRemoveCommand() *cobra.Command { func NewRemoveCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "rm [NAME]", Use: "rm [NAME]",
Short: "Remove a cluster.", Short: "Remove a cluster.",
Aliases: []string{"remove", "delete"}, Args: cobra.MaximumNArgs(1),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
p := cmd.Context().Value("provisioner").(*ucind.Provisioner) p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
@@ -48,7 +48,10 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
if opts.machine != "" { if opts.machine != "" {
// If a specific machine is requested, use it to get the Caddy configuration. // If a specific machine is requested, use it to get the Caddy configuration.
ctx = clusterClient.ProxySingleMachineContext(ctx, opts.machine) ctx, _, err = clusterClient.ProxyMachinesContext(ctx, []string{opts.machine})
if err != nil {
return err
}
} }
config, err := clusterClient.Caddy.GetConfig(ctx, nil) config, err := clusterClient.Caddy.GetConfig(ctx, nil)
@@ -12,7 +12,6 @@ func NewRootCommand() *cobra.Command {
cmd.AddCommand( cmd.AddCommand(
NewConfigCommand(), NewConfigCommand(),
NewDeployCommand(), NewDeployCommand(),
NewLogsCommand(),
) )
return cmd return cmd
} }
@@ -14,7 +14,6 @@ func NewConnectionCommand() *cobra.Command {
Use: "connection", Use: "connection",
Aliases: []string{"conn"}, Aliases: []string{"conn"},
Short: "Choose a new default connection for the current context.", Short: "Choose a new default connection for the current context.",
Args: cobra.NoArgs,
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)
return selectConnection(uncli) return selectConnection(uncli)
@@ -5,7 +5,6 @@ import (
"maps" "maps"
"slices" "slices"
"charm.land/lipgloss/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -16,7 +15,6 @@ func NewListCommand() *cobra.Command {
Use: "ls", Use: "ls",
Aliases: []string{"list"}, Aliases: []string{"list"},
Short: "List available cluster contexts.", Short: "List available cluster contexts.",
Args: cobra.NoArgs,
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)
return list(uncli) return list(uncli)
@@ -51,6 +49,6 @@ func list(uncli *cli.CLI) error {
t.Row(name, current, fmt.Sprintf("%d", connCount)) t.Row(name, current, fmt.Sprintf("%d", connCount))
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
@@ -10,7 +10,6 @@ func NewRootCommand() *cobra.Command {
Use: "ctx", Use: "ctx",
Aliases: []string{"context"}, Aliases: []string{"context"},
Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.", Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.",
Args: cobra.NoArgs,
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)
return selectContext(uncli) return selectContext(uncli)
@@ -11,7 +11,6 @@ func NewShowCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "show", Use: "show",
Short: "Show current cluster context.", Short: "Show current cluster context.",
Args: cobra.NoArgs,
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)
return show(uncli) return show(uncli)
@@ -8,7 +8,6 @@ import (
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -18,7 +17,6 @@ func NewUseCommand() *cobra.Command {
Short: "Switch to a different cluster context.", Short: "Switch to a different cluster context.",
Long: "Switch to a different cluster context. If no context is provided, " + Long: "Switch to a different cluster context. If no context is provided, " +
"a list of available contexts will be displayed for selection.", "a list of available contexts will be displayed for selection.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -51,10 +49,6 @@ func selectContext(uncli *cli.CLI) error {
if len(uncli.Config.Contexts) == 0 { if len(uncli.Config.Contexts) == 0 {
return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path()) return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path())
} }
if !tui.IsTerminalAvailable() {
return fmt.Errorf("cannot select a context interactively without a terminal. " +
"Pass the context name explicitly: uc ctx use CONTEXT")
}
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts)) contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
+1 -9
View File
@@ -163,14 +163,6 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
fmt.Println() fmt.Println()
} }
// Resolve 'secret://name' references to actual secret values before creating a deployment.
if compose.HasCommandSecretRefs(project) {
fmt.Fprintln(os.Stderr, "Resolving secrets...")
}
if err = compose.ResolveSecrets(ctx, project); err != nil {
return fmt.Errorf("resolve secrets: %w", err)
}
strategy := &deploy.RollingStrategy{ strategy := &deploy.RollingStrategy{
ForceRecreate: opts.recreate, ForceRecreate: opts.recreate,
SkipHealthMonitor: opts.skipHealth, SkipHealthMonitor: opts.skipHealth,
@@ -211,7 +203,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
// Ask for plan confirmation before proceeding with the deployment unless auto-confirmed with --yes. // Ask for plan confirmation before proceeding with the deployment unless auto-confirmed with --yes.
if !opts.yes { if !opts.yes {
if !tui.IsTerminalAvailable() { if !tui.IsStdinTerminal() {
return errors.New("cannot ask to confirm deployment plan in non-interactive mode, " + return errors.New("cannot ask to confirm deployment plan in non-interactive mode, " +
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm") "use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
} }
@@ -5,7 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/cmd/uc/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
+18 -8
View File
@@ -88,6 +88,19 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
} }
defer clusterClient.Close() defer clusterClient.Close()
// Get all machines to create ID to name mapping.
allMachines, err := clusterClient.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machineIDToName := make(map[string]string)
for _, machineMember := range allMachines {
if machineMember.Machine != nil && machineMember.Machine.Id != "" && machineMember.Machine.Name != "" {
machineIDToName[machineMember.Machine.Id] = machineMember.Machine.Name
}
}
machines := cli.ExpandCommaSeparatedValues(opts.machines) machines := cli.ExpandCommaSeparatedValues(opts.machines)
clusterImages, err := clusterClient.ListImages(ctx, api.ImageFilter{ clusterImages, err := clusterClient.ListImages(ctx, api.ImageFilter{
@@ -102,14 +115,11 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
var rows []imageRow var rows []imageRow
for _, machineImages := range clusterImages { for _, machineImages := range clusterImages {
if err := machineImages.Error(); err != nil {
tui.PrintWarning(fmt.Sprintf("failed to list images on machine '%s': %s",
machineImages.Metadata.MachineName, err))
continue
}
// Get machine name for better readability. // Get machine name for better readability.
machineName := machineImages.Metadata.MachineName machineName := machineImages.Metadata.Machine
if m := allMachines.FindByNameOrID(machineName); m != nil {
machineName = m.Machine.Name
}
store := "docker" store := "docker"
if machineImages.ContainerdStore { if machineImages.ContainerdStore {
@@ -179,7 +189,7 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
}) })
// Print the images in a table format. // Print the images in a table format.
lipgloss.Println(formatImageTable(rows)) fmt.Println(formatImageTable(rows))
return nil return nil
} }
+1 -1
View File
@@ -3,7 +3,7 @@ package main
import ( import (
"strings" "strings"
"github.com/psviderski/uncloud/cmd/uc/image" "github.com/psviderski/uncloud/cmd/uncloud/image"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -8,9 +8,10 @@ import (
"strings" "strings"
"time" "time"
"charm.land/huh/v2/spinner"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uc/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
@@ -28,8 +29,6 @@ type addOptions struct {
sshKey string sshKey string
version string version string
wgEndpoints []string wgEndpoints []string
wgPort int
wgMTU int
yes bool yes bool
} }
@@ -71,8 +70,7 @@ Connection methods:
return add(cmd.Context(), uncli, remoteMachine, opts) return add(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVarP(&opts.name, "name", "n", "", cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
"Assign a name to the machine. (default is the machine's hostname)")
cmd.Flags().BoolVar( cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false, &opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.", "Don't deploy Caddy reverse proxy service to the machine.",
@@ -98,21 +96,13 @@ Connection methods:
) )
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil, &opts.wgEndpoints, "wg-endpoint", nil,
"WireGuard endpoint address that other machines in the cluster should use to establish "+ fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+
"WireGuard connections\n"+ "WireGuard connections\n"+
"to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+ "to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.\n"+ "Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is %d if omitted.\n", network.WireGuardPort)+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+ "Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
"Defaults to the auto-detected public and routable machine IPs.", "Defaults to the auto-detected public and routable machine IPs.",
) )
cmd.Flags().IntVar(
&opts.wgMTU, "wg-mtu", 0,
"MTU of the WireGuard network interface on the machine. (default auto-detects the optimal value)",
)
cmd.Flags().IntVar(
&opts.wgPort, "wg-port", network.DefaultWireGuardPort,
"UDP port WireGuard listens on for incoming connections from other machines.",
)
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+ "Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]") "Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
@@ -135,26 +125,17 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
publicIP = &ip publicIP = &ip
} }
if opts.wgPort < 1 || opts.wgPort > 65535 {
return fmt.Errorf("invalid WireGuard port %d: must be between 1 and 65535", opts.wgPort)
}
if opts.wgMTU != 0 && (opts.wgMTU < network.MinWireGuardMTU || opts.wgMTU > 65535) {
return fmt.Errorf("invalid WireGuard MTU %d: must be 0 (auto-detect) or between %d and 65535",
opts.wgMTU, network.MinWireGuardMTU)
}
addOpts := cli.AddMachineOptions{ addOpts := cli.AddMachineOptions{
MachineName: opts.name, MachineName: opts.name,
PublicIP: publicIP, PublicIP: publicIP,
RemoteMachine: remoteMachine, RemoteMachine: remoteMachine,
SkipInstall: opts.noInstall, SkipInstall: opts.noInstall,
Version: opts.version, Version: opts.version,
WireguardMTU: opts.wgMTU,
WireguardPort: opts.wgPort,
AutoConfirm: opts.yes, AutoConfirm: opts.yes,
} }
if len(opts.wgEndpoints) > 0 { if len(opts.wgEndpoints) > 0 {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints) expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded, uint16(opts.wgPort)) endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil { if err != nil {
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err) return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
} }
@@ -173,9 +154,19 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
} }
// Wait for the cluster to be initialised on the machine 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.
err = tui.RunSpinner(ctx, "Waiting for the machine to join the cluster...", func(ctx context.Context) error { err = spinner.New().
return machineClient.WaitClusterReady(ctx, 5*time.Minute) Title(" Waiting for the machine to join the cluster...").
}) Type(spinner.MiniDot).
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
return &spinner.Styles{
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
Title: lipgloss.NewStyle(),
}
})).
ActionWithErr(func(ctx context.Context) error {
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
}).
Run()
if err != nil { if err != nil {
return fmt.Errorf("wait for machine to join the cluster: %w", err) return fmt.Errorf("wait for machine to join the cluster: %w", err)
} }
@@ -7,12 +7,13 @@ import (
"strings" "strings"
"time" "time"
"charm.land/huh/v2/spinner"
"charm.land/lipgloss/v2"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uc/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uc/dns" "github.com/psviderski/uncloud/cmd/uncloud/dns"
"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/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
@@ -32,8 +33,6 @@ type initOptions struct {
sshKey string sshKey string
version string version string
wgEndpoints []string wgEndpoints []string
wgPort int
wgMTU int
yes bool yes bool
} }
@@ -101,7 +100,7 @@ Connection methods:
"API endpoint for the Uncloud DNS service.") "API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.name, "name", "n", "", &opts.name, "name", "n", "",
"Assign a name to the machine. (default is the machine's hostname)", "Assign a name to the machine.",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.network, "network", cluster.DefaultNetwork.String(), &opts.network, "network", cluster.DefaultNetwork.String(),
@@ -136,21 +135,13 @@ Connection methods:
) )
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil, &opts.wgEndpoints, "wg-endpoint", nil,
"WireGuard endpoint address that other machines in the cluster should use to establish "+ fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+
"WireGuard connections\n"+ "WireGuard connections\n"+
"to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+ "to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.\n"+ "Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is %d if omitted.\n", network.WireGuardPort)+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+ "Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
"Defaults to the auto-detected public and routable machine IPs.", "Defaults to the auto-detected public and routable machine IPs.",
) )
cmd.Flags().IntVar(
&opts.wgMTU, "wg-mtu", 0,
"MTU of the WireGuard network interface on the machine. (default auto-detects the optimal value)",
)
cmd.Flags().IntVar(
&opts.wgPort, "wg-port", network.DefaultWireGuardPort,
"UDP port WireGuard listens on for incoming connections from other machines.",
)
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+ "Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]") "Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
@@ -187,13 +178,6 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
publicIP = &ip publicIP = &ip
} }
if opts.wgPort < 1 || opts.wgPort > 65535 {
return fmt.Errorf("invalid WireGuard port %d: must be between 1 and 65535", opts.wgPort)
}
if opts.wgMTU != 0 && (opts.wgMTU < network.MinWireGuardMTU || opts.wgMTU > 65535) {
return fmt.Errorf("invalid WireGuard MTU %d: must be 0 (auto-detect) or between %d and 65535",
opts.wgMTU, network.MinWireGuardMTU)
}
initOpts := cli.InitClusterOptions{ initOpts := cli.InitClusterOptions{
Context: opts.context, Context: opts.context,
MachineName: opts.name, MachineName: opts.name,
@@ -202,13 +186,11 @@ 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,
WireguardMTU: opts.wgMTU,
WireguardPort: opts.wgPort,
AutoConfirm: opts.yes, AutoConfirm: opts.yes,
} }
if len(opts.wgEndpoints) > 0 { if len(opts.wgEndpoints) > 0 {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints) expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded, uint16(opts.wgPort)) endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil { if err != nil {
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err) return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
} }
@@ -224,9 +206,19 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
// Since the cluster API needs a few moments to become ready after cluster initialisation, // 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 // 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. // as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error { err = spinner.New().
return client.WaitClusterReady(ctx, 1*time.Minute) Title(" Waiting for the cluster to be ready...").
}) Type(spinner.MiniDot).
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
return &spinner.Styles{
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
Title: lipgloss.NewStyle(),
}
})).
ActionWithErr(func(ctx context.Context) error {
return client.WaitClusterReady(ctx, 1*time.Minute)
}).
Run()
if err != nil { if err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err) return fmt.Errorf("wait for cluster to be ready: %w", err)
} }
@@ -3,12 +3,10 @@ package machine
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/logs" "github.com/psviderski/uncloud/internal/cli/logs"
"github.com/psviderski/uncloud/internal/journal"
"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"
@@ -20,14 +18,14 @@ func NewLogsCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "logs [SERVICE...]", Use: "logs [SERVICE...]",
Aliases: []string{"log"}, Aliases: []string{"log"},
Short: "View system service logs.", Short: "View systemd service logs.",
Long: `View logs from the specified system service(s) across all machines in the cluster. Long: `View logs from the specified systemd service(s) across all machines in the cluster.
Use -m to restrict to specific machines. Use -m to restrict to specific machines.
Supported services: Supported services:
corrosion the Corrosion distributed state store uncloud the Uncloud daemon
docker the Docker daemon docker the Docker daemon
uncloud the Uncloud daemon uncloud-corrosion the Corrosion distributed state store
If no services are specified, streams logs from the uncloud service.`, If no services are specified, streams logs from the uncloud service.`,
Example: ` # View recent logs for the uncloud service. Example: ` # View recent logs for the uncloud service.
@@ -38,7 +36,7 @@ If no services are specified, streams logs from the uncloud service.`,
uc machine logs -f uncloud uc machine logs -f uncloud
# View logs from multiple services. # View logs from multiple services.
uc machine logs uncloud docker corrosion uc machine logs uncloud docker uncloud-corrosion
# Show last 20 lines per machine (default is 100). # Show last 20 lines per machine (default is 100).
uc machine logs -n 20 docker uc machine logs -n 20 docker
@@ -50,7 +48,7 @@ If no services are specified, streams logs from the uncloud service.`,
uc machine logs --since 3h --until 1h30m docker uc machine logs --since 3h --until 1h30m docker
# View logs only from specific machines. # View logs only from specific machines.
uc machine logs -m machine1,machine2 uncloud corrosion`, uc machine logs -m machine1,machine2 uncloud uncloud-corrosion`,
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)
return runLogs(cmd.Context(), uncli, args, options) return runLogs(cmd.Context(), uncli, args, options)
@@ -58,19 +56,17 @@ If no services are specified, streams logs from the uncloud service.`,
} }
cmd.Flags().AddFlagSet(logs.Flags(&options)) cmd.Flags().AddFlagSet(logs.Flags(&options))
completion.MachinesFlag(cmd)
return cmd return cmd
} }
func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.Options) error { func runLogs(ctx context.Context, uncli *cli.CLI, units []string, opts logs.Options) error {
if len(services) == 0 { if len(units) == 0 {
services = []string{api.SystemServiceUncloud} units = []string{journal.UnitUncloud}
} }
for _, service := range services {
if !slices.Contains(api.SystemServices, service) { for _, unit := range units {
return fmt.Errorf("invalid system service '%s'; valid services: %s", if !journal.ValidUnit(unit) {
service, strings.Join(api.SystemServices, ", ")) return fmt.Errorf("invalid systemd service '%s'", unit)
} }
} }
@@ -105,27 +101,27 @@ func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.O
machineNames = append(machineNames, m.Machine.Name) machineNames = append(machineNames, m.Machine.Name)
} }
// Collect one log stream per service. MachineLogs merges across machines internally. // Collect one log stream per unit. MachineLogs merges across machines internally.
serviceStreams := make([]<-chan api.ServiceLogEntry, 0, len(services)) unitStreams := make([]<-chan api.ServiceLogEntry, 0, len(units))
for _, service := range services { for _, unit := range units {
ch, err := c.MachineLogs(ctx, service, logsOpts) ch, err := c.MachineLogs(ctx, unit, logsOpts)
if err != nil { if err != nil {
return fmt.Errorf("stream logs for system service '%s': %w", service, err) return fmt.Errorf("stream logs for systemd service '%s': %w", unit, err)
} }
serviceStreams = append(serviceStreams, ch) unitStreams = append(unitStreams, ch)
} }
var stream <-chan api.ServiceLogEntry var stream <-chan api.ServiceLogEntry
if len(serviceStreams) == 1 { if len(unitStreams) == 1 {
stream = serviceStreams[0] stream = unitStreams[0]
} else { } else {
// Each MachineLogs stream already runs its own inner merger with stall detection, // Each MachineLogs stream already runs its own inner merger with stall detection,
// so the outer merger across services skips it to avoid duplicate warnings. // so the outer merger across units skips it to avoid duplicate warnings.
merger := client.NewLogMerger(serviceStreams, client.LogMergerOptions{}) merger := client.NewLogMerger(unitStreams, client.LogMergerOptions{})
stream = merger.Stream() stream = merger.Stream()
} }
formatter := logs.NewFormatter(machineNames, services, opts.UTC) formatter := logs.NewFormatter(machineNames, units, opts.UTC)
// Print merged logs. // Print merged logs.
for entry := range stream { for entry := range stream {
@@ -2,12 +2,10 @@ package machine
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/netip" "net/netip"
"strings" "strings"
"charm.land/lipgloss/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
@@ -15,29 +13,19 @@ import (
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
var output string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ls", Use: "ls",
Aliases: []string{"list"}, Aliases: []string{"list"},
Short: "List machines in a cluster.", Short: "List machines in a cluster.",
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)
return list(cmd.Context(), uncli, output) return list(cmd.Context(), uncli)
}, },
} }
cmd.Flags().StringVarP(&output, "output", "o", "",
"Output format: 'json' or empty for a human-readable table.")
return cmd return cmd
} }
func list(ctx context.Context, uncli *cli.CLI, output string) error { func list(ctx context.Context, uncli *cli.CLI) error {
if output != "" && output != "json" {
return fmt.Errorf("unsupported output format '%s' (supported: json)", output)
}
client, err := uncli.ConnectCluster(ctx) client, err := uncli.ConnectCluster(ctx)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
@@ -49,19 +37,9 @@ func list(ctx context.Context, uncli *cli.CLI, output string) error {
return fmt.Errorf("list machines: %w", err) return fmt.Errorf("list machines: %w", err)
} }
if output == "json" {
data, err := json.MarshalIndent(machines.ToNative(), "", " ")
if err != nil {
return fmt.Errorf("marshal machines: %w", err)
}
fmt.Println(string(data))
return nil
}
// Print the list of machines in a table format. // Print the list of machines in a table format.
t := tui.NewTable() t := tui.NewTable()
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", "MACHINE ID")
"OS", "KERNEL", "ARCH", "DOCKER", "VERSION")
for _, member := range machines { for _, member := range machines {
m := member.Machine m := member.Machine
@@ -80,46 +58,17 @@ func list(ctx context.Context, uncli *cli.CLI, output string) error {
endpoints[i] = addrPort.String() endpoints[i] = addrPort.String()
} }
arch := "-"
if m.Arch != "" {
arch = m.Arch
}
osName := "-"
if m.OsPrettyName != "" {
osName = m.OsPrettyName
}
kernel := "-"
if m.KernelVersion != "" {
kernel = m.KernelVersion
}
daemonVersion := "-"
if m.DaemonVersion != "" {
daemonVersion = m.DaemonVersion
}
dockerVersion := "-"
if m.DockerVersion != "" {
dockerVersion = m.DockerVersion
}
t.Row( t.Row(
m.Name, m.Name,
capitalise(member.State.String()), capitalise(member.State.String()),
subnet.String(), subnet.String(),
publicIP, publicIP,
strings.Join(endpoints, tui.Faint.Render(", ")), strings.Join(endpoints, tui.Faint.Render(", ")),
osName, member.Machine.Id,
kernel,
arch,
dockerVersion,
daemonVersion,
) )
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
@@ -63,16 +63,15 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
} }
defer client.Close() defer client.Close()
// Verify the machine exists in the cluster. // Verify the machine exists and list all service containers on it including stopped ones.
member, err := client.InspectMachine(ctx, nameOrID) mctx, machines, err := client.ProxyMachinesContext(ctx, []string{nameOrID})
if err != nil { if err != nil {
return fmt.Errorf("inspect machine '%s': %w", nameOrID, err) return err
} }
m := member.Machine if len(machines) == 0 {
return fmt.Errorf("machine '%s' not found in the cluster", nameOrID)
// Create a proxy context for the machine being removed. }
// This is used for calls that need to run directly on that machine. m := machines[0].Machine
rmCtx := client.ProxySingleMachineContext(ctx, m.Id)
// Verify if the machine being removed is the proxy machine we're connected to. // Verify if the machine being removed is the proxy machine we're connected to.
proxyMachine, err := client.MachineClient.Inspect(ctx, nil) proxyMachine, err := client.MachineClient.Inspect(ctx, nil)
@@ -104,7 +103,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
if reset { if reset {
// Check if the machine is up and has service containers. // Check if the machine is up and has service containers.
listOpts := container.ListOptions{All: true} listOpts := container.ListOptions{All: true}
machineContainers, err := client.Docker.ListServiceContainers(rmCtx, "", listOpts) machineContainers, err := client.Docker.ListServiceContainers(mctx, "", listOpts)
if err == nil { if err == nil {
reachable = true reachable = true
containers = machineContainers[0].Containers containers = machineContainers[0].Containers
@@ -114,7 +113,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
plural = "s" plural = "s"
} }
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name) fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
lipgloss.Println(formatContainerTree(containers)) fmt.Println(formatContainerTree(containers))
fmt.Println() fmt.Println()
fmt.Println("This will remove all service containers from the machine, remove it from the cluster, " + fmt.Println("This will remove all service containers from the machine, remove it from the cluster, " +
"and reset it to the uninitialised state.") "and reset it to the uninitialised state.")
@@ -151,22 +150,20 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
fmt.Println() fmt.Println()
} }
// Initiate reset before removing the machine from the cluster to stop it from updating the cluster store.
// This is still optimistic as Reset only triggers the reset process that runs asynchronously.
if reset && reachable {
_, err = client.MachineClient.Reset(rmCtx, &pb.ResetRequest{})
if err != nil {
tui.PrintWarning(fmt.Sprintf("Failed to reset machine: %v\n", err))
} else {
fmt.Println("Machine reset initiated and will complete in the background.")
}
}
if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil { if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil {
return fmt.Errorf("remove machine from cluster: %w", err) return fmt.Errorf("remove machine from cluster: %w", err)
} }
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name) fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
if reset && reachable {
_, err = client.MachineClient.Reset(mctx, &pb.ResetRequest{})
if err != nil {
fmt.Printf("WARNING: Failed to reset machine: %v\n", err)
} else {
fmt.Println("Machine reset initiated and will complete in the background.")
}
}
// Remove the connection to the machine from the uncloud config if it exists. // Remove the connection to the machine from the uncloud config if it exists.
if uncli.Config != nil { if uncli.Config != nil {
contextName := uncli.ContextOverrideOrCurrent() contextName := uncli.ContextOverrideOrCurrent()
@@ -6,7 +6,6 @@ import (
"sort" "sort"
"time" "time"
"charm.land/lipgloss/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -39,7 +38,10 @@ func rtt(ctx context.Context, uncli *cli.CLI) error {
defer client.Close() defer client.Close()
// Setup context to proxy request to all machines. // Setup context to proxy request to all machines.
ctx = client.ProxyMachinesContext(ctx, nil) ctx, _, err = client.ProxyMachinesContext(ctx, nil)
if err != nil {
return fmt.Errorf("setup proxy context: %w", err)
}
resp, err := client.MachineClient.InspectMachine(ctx, &emptypb.Empty{}) resp, err := client.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
@@ -49,15 +51,6 @@ func rtt(ctx context.Context, uncli *cli.CLI) error {
// Map machine IDs to names for display from the response. // Map machine IDs to names for display from the response.
machineNames := make(map[string]string) machineNames := make(map[string]string)
for _, m := range resp.Machines { for _, m := range resp.Machines {
// NOTE: Metadata should never be nil in practice. This is legacy fallback that will be removed.
if m.Metadata == nil {
tui.PrintWarning("metadata is missing in response from unknown server")
continue
}
if m.Metadata.Error != "" {
tui.PrintWarning(fmt.Sprintf("failed to inspect machine '%s': %s", m.Metadata.MachineName, m.Metadata.Error))
continue
}
if m.Machine == nil { if m.Machine == nil {
continue continue
} }
@@ -108,7 +101,7 @@ func rtt(ctx context.Context, uncli *cli.CLI) error {
t.Row(r.machine, r.peer, tui.FormatRTT(r.median), formatRTTStdDev(r.stdDev)) t.Row(r.machine, r.peer, tui.FormatRTT(r.median), formatRTTStdDev(r.stdDev))
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
@@ -70,8 +70,7 @@ At least one flag must be specified to perform an update.`,
fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+ fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+
"WireGuard connections\n"+ "WireGuard connections\n"+
"to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+ "to this machine. This doesn't change the address/port WireGuard listens on the machine.\n"+
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is %d if omitted.\n", "Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is %d if omitted.\n", network.WireGuardPort)+
network.DefaultWireGuardPort)+
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.", "Multiple endpoints can be specified by repeating the flag or using a comma-separated list.",
) )
@@ -90,13 +89,16 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
} }
defer client.Close() defer client.Close()
// Resolve the machine to capture its current configuration for the before/after report and to validate existence. // First, resolve the machine to get its ID
machine, err := client.InspectMachine(ctx, machineNameOrID) machine, err := client.InspectMachine(ctx, machineNameOrID)
if err != nil { if err != nil {
return fmt.Errorf("find machine: %w", err) return fmt.Errorf("find machine: %w", err)
} }
req := &pb.UpdateMachineRequest{} // Build the update request
req := &pb.UpdateMachineRequest{
MachineId: machine.Machine.Id,
}
if opts.name != "" { if opts.name != "" {
req.Name = &opts.name req.Name = &opts.name
@@ -119,7 +121,7 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
// Parse and set endpoints if the flag was explicitly provided. // Parse and set endpoints if the flag was explicitly provided.
if cmd.Flags().Changed("wg-endpoint") { if cmd.Flags().Changed("wg-endpoint") {
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints) expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
endpoints, err := cli.ParseWireGuardEndpoints(expanded, network.DefaultWireGuardPort) endpoints, err := cli.ParseWireGuardEndpoints(expanded)
if err != nil { if err != nil {
return err return err
} }
@@ -129,7 +131,8 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
req.Endpoints = endpoints req.Endpoints = endpoints
} }
updatedMachine, err := client.UpdateMachine(ctx, machine.Machine.Id, req) // Perform the update operation
updatedMachine, err := client.UpdateMachine(ctx, req)
if err != nil { if err != nil {
return fmt.Errorf("update machine: %w", err) return fmt.Errorf("update machine: %w", err)
} }
+16 -13
View File
@@ -8,17 +8,17 @@ import (
"os" "os"
"strings" "strings"
"github.com/psviderski/uncloud/cmd/uc/caddy" "charm.land/lipgloss/v2"
cmdcontext "github.com/psviderski/uncloud/cmd/uc/context" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uc/dns" cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
"github.com/psviderski/uncloud/cmd/uc/image" "github.com/psviderski/uncloud/cmd/uncloud/dns"
cmdmachine "github.com/psviderski/uncloud/cmd/uc/machine" "github.com/psviderski/uncloud/cmd/uncloud/image"
"github.com/psviderski/uncloud/cmd/uc/service" cmdmachine "github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uc/volume" "github.com/psviderski/uncloud/cmd/uncloud/service"
"github.com/psviderski/uncloud/cmd/uc/wg" "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/cli/tui"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/log" "github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
@@ -39,6 +39,7 @@ func main() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uc", Use: "uc",
Short: "A CLI tool for managing Uncloud resources such as machines, services, and volumes.", Short: "A CLI tool for managing Uncloud resources such as machines, services, and volumes.",
Version: version.String(),
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
@@ -112,11 +113,15 @@ func main() {
defaultHelpFunc(c, args) defaultHelpFunc(c, args)
// Only show links for the root 'uc' command. // Only show links for the root 'uc' command.
if c.Name() == "uc" { if c.Name() == "uc" {
urlStyle := lipgloss.NewStyle().
Underline(true).
Foreground(lipgloss.Color("12")) // light blue
fmt.Fprintln(c.OutOrStdout()) fmt.Fprintln(c.OutOrStdout())
fmt.Fprintf(c.OutOrStdout(), "Learn more about Uncloud: %s\n", fmt.Fprintf(c.OutOrStdout(), "Learn more about Uncloud: %s\n",
tui.URLStyle.Render(version.DocsURL)) urlStyle.Render("https://uncloud.run/docs"))
fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n", fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n",
tui.URLStyle.Render(version.DiscordURL)) urlStyle.Render("https://uncloud.run/discord"))
} }
}) })
@@ -131,7 +136,6 @@ func main() {
NewDocsCommand(), NewDocsCommand(),
NewImagesCommand(), NewImagesCommand(),
NewPsCommand(), NewPsCommand(),
NewProxyCommand(),
caddy.NewRootCommand(), caddy.NewRootCommand(),
cmdcontext.NewRootCommand(), cmdcontext.NewRootCommand(),
dns.NewRootCommand(), dns.NewRootCommand(),
@@ -147,7 +151,6 @@ func main() {
service.NewScaleCommand("service"), service.NewScaleCommand("service"),
service.NewStartCommand("service"), service.NewStartCommand("service"),
service.NewStopCommand("service"), service.NewStopCommand("service"),
NewVersionCommand(),
volume.NewRootCommand(), volume.NewRootCommand(),
wg.NewRootCommand(), wg.NewRootCommand(),
) )
+48 -14
View File
@@ -6,6 +6,7 @@ import (
"sort" "sort"
"time" "time"
"charm.land/huh/v2/spinner"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/go-units" "github.com/docker/go-units"
@@ -82,10 +83,20 @@ func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
defer clusterClient.Close() defer clusterClient.Close()
var containers []containerInfo var containers []containerInfo
err = tui.RunSpinner(ctx, "Collecting container info...", func(ctx context.Context) error { err = spinner.New().
containers, err = collectContainers(ctx, clusterClient) Title(" Collecting container info...").
return err Type(spinner.MiniDot).
}) WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
return &spinner.Styles{
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
Title: lipgloss.NewStyle(),
}
})).
ActionWithErr(func(ctx context.Context) error {
containers, err = collectContainers(ctx, clusterClient)
return err
}).
Run()
if err != nil { if err != nil {
return fmt.Errorf("collect containers: %w", err) return fmt.Errorf("collect containers: %w", err)
} }
@@ -182,12 +193,23 @@ func printContainers(containers []containerInfo) error {
} }
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo, error) { func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo, error) {
listCtx := cli.ProxyMachinesContext(ctx, nil) listCtx, machines, err := cli.ProxyMachinesContext(ctx, nil)
if err != nil {
return nil, fmt.Errorf("proxy machines context: %w", err)
}
// Create a map of IP to machine name for resolving response metadata
machinesNamesByIP := make(map[string]string)
for _, m := range machines {
if addr, err := m.Machine.Network.ManagementIp.ToAddr(); err == nil {
machinesNamesByIP[addr.String()] = m.Machine.Name
}
}
// List all service containers across all machines in the cluster. // List all service containers across all machines in the cluster.
machineContainers, err := cli.Docker.ListServiceContainers( machineContainers, err := cli.Docker.ListServiceContainers(
@@ -199,17 +221,29 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
var containers []containerInfo var containers []containerInfo
for _, msc := range machineContainers { for _, msc := range machineContainers {
// NOTE: Metadata should never be nil in practice. This is legacy fallback that will be removed. // Metadata can be nil if the request was broadcasted to only one machine.
if msc.Metadata == nil { if msc.Metadata == nil && len(machineContainers) > 1 {
tui.PrintWarning("metadata is missing in response from unknown server") return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
continue
} }
machineName := msc.Metadata.MachineName machineName := "unknown"
if msc.Metadata != nil {
var ok bool
machineName, ok = machinesNamesByIP[msc.Metadata.Machine]
if !ok {
// Fallback to machine's IP as name.
machineName = msc.Metadata.Machine
}
} else {
// Fallback to the first available machine name.
if len(machines) > 0 {
machineName = machines[0].Machine.Name
}
}
if msc.Metadata.Error != "" { if msc.Metadata != nil && msc.Metadata.Error != "" {
tui.PrintWarning(fmt.Sprintf("failed to list service containers on machine %s: %s", tui.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
machineName, msc.Metadata.Error)) msc.Metadata.Error))
continue continue
} }
+441
View File
@@ -0,0 +1,441 @@
package main
import (
"context"
"encoding/json"
"net/netip"
"testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
// mockDockerClient implements pb.DockerClient
type mockDockerClient struct {
pb.DockerClient // Embed to avoid implementing all methods
listResp *pb.ListServiceContainersResponse
listErr error
}
func (m *mockDockerClient) ListServiceContainers(ctx context.Context, in *pb.ListServiceContainersRequest, opts ...grpc.CallOption) (*pb.ListServiceContainersResponse, error) {
return m.listResp, m.listErr
}
// mockClusterClient implements pb.ClusterClient
type mockClusterClient struct {
pb.ClusterClient // Embed to avoid implementing all methods
machinesResp *pb.ListMachinesResponse
machinesErr error
}
func (m *mockClusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ListMachinesResponse, error) {
return m.machinesResp, m.machinesErr
}
func TestCollectContainers_NilMetadata(t *testing.T) {
// Setup container data
containerData := map[string]any{
"Id": "container1",
"Name": "test-container",
"Config": map[string]any{
"Image": "test-image",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON, err := json.Marshal(containerData)
require.NoError(t, err)
serviceSpecJSON, err := json.Marshal(map[string]any{})
require.NoError(t, err)
// Setup mocks
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: nil, // Simulating the issue: nil metadata
Containers: []*pb.ServiceContainer{
{
Container: containerJSON,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
machineIP := "10.0.0.1"
mockCluster := &mockClusterClient{
machinesResp: &pb.ListMachinesResponse{
Machines: []*pb.MachineMember{
{
Machine: &pb.MachineInfo{
Name: "machine-1",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr(machineIP)),
},
},
State: pb.MachineMember_UP,
},
},
},
}
// Construct client with mocks
cli := &client.Client{
Docker: &docker.Client{
GRPCClient: mockDocker,
},
ClusterClient: mockCluster,
}
// Execute
containers, err := collectContainers(context.Background(), cli)
require.NoError(t, err)
// Verify
assert.Len(t, containers, 1)
if len(containers) > 0 {
c := containers[0]
assert.Equal(t, "container1", c.id)
assert.Equal(t, "machine-1", c.machineName, "Should fall back to the single machine name when metadata is nil")
}
}
func TestCollectContainers_NilMetadata_MultipleMachines_Error(t *testing.T) {
// If we have multiple machines but receive nil metadata, it should return an error as it is ambiguous
// Setup container data
containerData1 := map[string]any{
"Id": "container1",
}
containerJSON1, _ := json.Marshal(containerData1)
containerData2 := map[string]any{
"Id": "container2",
"Config": map[string]any{
"Image": "test-image",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON2, _ := json.Marshal(containerData2)
serviceSpecJSON, _ := json.Marshal(map[string]any{})
// Setup mocks
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: nil, // Nil metadata
Containers: []*pb.ServiceContainer{
{
Container: containerJSON1,
ServiceSpec: serviceSpecJSON,
},
},
},
{
Metadata: &pb.Metadata{Machine: "10.0.0.2"},
Containers: []*pb.ServiceContainer{
{
Container: containerJSON2,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
mockCluster := &mockClusterClient{
machinesResp: &pb.ListMachinesResponse{
Machines: []*pb.MachineMember{
{
Machine: &pb.MachineInfo{
Name: "machine-1",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
},
},
},
{
Machine: &pb.MachineInfo{
Name: "machine-2",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.2")),
},
},
},
},
},
}
// Construct client with mocks
cli := &client.Client{
Docker: &docker.Client{
GRPCClient: mockDocker,
},
ClusterClient: mockCluster,
}
// Execute
_, err := collectContainers(context.Background(), cli)
require.Error(t, err)
assert.Contains(t, err.Error(), "metadata is missing for a machine response")
}
func TestCollectContainers_MetadataPresent_MultipleMachines(t *testing.T) {
// Verify correct mapping of containers to machines when metadata is present
// Setup container data
containerData1 := map[string]any{
"Id": "container1",
"Name": "container-1",
"Config": map[string]any{
"Image": "image-1",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON1, _ := json.Marshal(containerData1)
containerData2 := map[string]any{
"Id": "container2",
"Name": "container-2",
"Config": map[string]any{
"Image": "image-2",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON2, _ := json.Marshal(containerData2)
serviceSpecJSON, _ := json.Marshal(map[string]any{})
// Setup mocks
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: &pb.Metadata{Machine: "10.0.0.1"},
Containers: []*pb.ServiceContainer{
{
Container: containerJSON1,
ServiceSpec: serviceSpecJSON,
},
},
},
{
Metadata: &pb.Metadata{Machine: "10.0.0.2"},
Containers: []*pb.ServiceContainer{
{
Container: containerJSON2,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
mockCluster := &mockClusterClient{
machinesResp: &pb.ListMachinesResponse{
Machines: []*pb.MachineMember{
{
Machine: &pb.MachineInfo{
Name: "machine-1",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
},
},
},
{
Machine: &pb.MachineInfo{
Name: "machine-2",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.2")),
},
},
},
},
},
}
cli := &client.Client{
Docker: &docker.Client{GRPCClient: mockDocker},
ClusterClient: mockCluster,
}
containers, err := collectContainers(context.Background(), cli)
require.NoError(t, err)
assert.Len(t, containers, 2)
// Order is not guaranteed by the map iteration in logic or parallel fetch (though here it's mocked sequential),
// but collectContainers just appends.
// We'll find them by ID.
for _, c := range containers {
if c.id == "container1" {
assert.Equal(t, "machine-1", c.machineName)
} else if c.id == "container2" {
assert.Equal(t, "machine-2", c.machineName)
} else {
t.Errorf("unexpected container id: %s", c.id)
}
}
}
func TestCollectContainers_NilMetadata_NoMachines(t *testing.T) {
// Case: 1 msc with nil metadata but no machines at all
containerData := map[string]any{
"Id": "container1",
"Config": map[string]any{
"Image": "test-image",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON, _ := json.Marshal(containerData)
serviceSpecJSON, _ := json.Marshal(map[string]any{})
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: nil,
Containers: []*pb.ServiceContainer{
{
Container: containerJSON,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
// No machines in cluster response
mockCluster := &mockClusterClient{
machinesResp: &pb.ListMachinesResponse{
Machines: []*pb.MachineMember{},
},
}
cli := &client.Client{
Docker: &docker.Client{GRPCClient: mockDocker},
ClusterClient: mockCluster,
}
containers, err := collectContainers(context.Background(), cli)
require.NoError(t, err)
assert.Len(t, containers, 1)
if len(containers) > 0 {
assert.Equal(t, "unknown", containers[0].machineName)
}
}
func TestCollectContainers_MetadataPresent_NotInMapping(t *testing.T) {
// Case: msc with metadata that is not in the IP-to-name mapping
containerData := map[string]any{
"Id": "container1",
"Config": map[string]any{
"Image": "test-image",
},
"State": map[string]any{
"Status": "running",
"StartedAt": "2023-01-01T12:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
},
"NetworkSettings": map[string]any{
"Networks": map[string]any{},
},
}
containerJSON, _ := json.Marshal(containerData)
serviceSpecJSON, _ := json.Marshal(map[string]any{})
mockDocker := &mockDockerClient{
listResp: &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Metadata: &pb.Metadata{Machine: "10.0.0.99"}, // Unknown IP
Containers: []*pb.ServiceContainer{
{
Container: containerJSON,
ServiceSpec: serviceSpecJSON,
},
},
},
},
},
}
mockCluster := &mockClusterClient{
machinesResp: &pb.ListMachinesResponse{
Machines: []*pb.MachineMember{
{
Machine: &pb.MachineInfo{
Name: "machine-1",
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
},
},
},
},
},
}
cli := &client.Client{
Docker: &docker.Client{GRPCClient: mockDocker},
ClusterClient: mockCluster,
}
containers, err := collectContainers(context.Background(), cli)
require.NoError(t, err)
assert.Len(t, containers, 1)
if len(containers) > 0 {
// Should fallback to the IP/string in metadata
assert.Equal(t, "10.0.0.99", containers[0].machineName)
}
}
@@ -6,7 +6,6 @@ import (
"slices" "slices"
"time" "time"
"charm.land/lipgloss/v2"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
@@ -55,6 +54,15 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
return fmt.Errorf("inspect service: %w", err) return fmt.Errorf("inspect service: %w", err)
} }
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machinesNamesByID := make(map[string]string)
for _, m := range machines {
machinesNamesByID[m.Machine.Id] = m.Machine.Name
}
fmt.Printf("Service ID: %s\n", svc.ID) fmt.Printf("Service ID: %s\n", svc.ID)
fmt.Printf("Name: %s\n", svc.Name) fmt.Printf("Name: %s\n", svc.Name)
fmt.Printf("Mode: %s\n", svc.Mode) fmt.Printf("Mode: %s\n", svc.Mode)
@@ -89,7 +97,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
for _, ctr := range allContainers { for _, ctr := range allContainers {
created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago" created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago"
machine := ctr.MachineName machine := machinesNamesByID[ctr.MachineID]
if machine == "" { if machine == "" {
machine = ctr.MachineID machine = ctr.MachineID
} }
@@ -127,6 +135,6 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
} }
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
@@ -59,7 +59,7 @@ If no services are specified, streams logs from all services defined in the Comp
uc logs -m machine1,machine2 web api`, uc logs -m machine1,machine2 web api`,
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)
return RunLogs(cmd.Context(), uncli, args, options) return runLogs(cmd.Context(), uncli, args, options)
}, },
GroupID: groupID, GroupID: groupID,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
@@ -74,10 +74,12 @@ If no services are specified, streams logs from all services defined in the Comp
cmd.Flags().AddFlagSet(logs.Flags(&options)) cmd.Flags().AddFlagSet(logs.Flags(&options))
completion.MachinesFlag(cmd) completion.MachinesFlag(cmd)
completion.MachinesFlag(cmd)
return cmd return cmd
} }
func RunLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Options) error { func runLogs(ctx context.Context, uncli *cli.CLI, args []string, opts logs.Options) error {
serviceArgs, err := logs.ParseServiceArgs(args) serviceArgs, err := logs.ParseServiceArgs(args)
if err != nil { if err != nil {
return err return err
@@ -6,7 +6,6 @@ import (
"slices" "slices"
"strings" "strings"
"charm.land/lipgloss/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
@@ -83,6 +82,6 @@ func list(ctx context.Context, uncli *cli.CLI) error {
t.Row(row...) t.Row(row...)
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
@@ -93,17 +93,15 @@ func NewRunCommand(groupID string) *cobra.Command {
"Give extended privileges to service containers. This is a security risk and should be used with caution.") "Give extended privileges to service containers. This is a security risk and should be used with caution.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil, cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+ "Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host\n"+ "Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+ "Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+ "and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+ "Examples:\n"+
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+ " -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+ " -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
// TODO: add support for publishing L4 tcp/udp ports. // TODO: add support for publishing L4 tcp/udp ports.
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+ //" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+ " -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
" -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address\n"+
" contained in the prefix 192.168.76.0/24")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing, cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').", fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever)) api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
@@ -136,7 +136,7 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
// Ask for confirmation unless auto-confirmed with --yes. // Ask for confirmation unless auto-confirmed with --yes.
if !opts.yes { if !opts.yes {
if !tui.IsTerminalAvailable() { if !tui.IsStdinTerminal() {
return errors.New("cannot ask to confirm scaling plan in non-interactive mode, " + return errors.New("cannot ask to confirm scaling plan in non-interactive mode, " +
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm") "use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
} }
@@ -6,7 +6,6 @@ import (
"slices" "slices"
"strings" "strings"
"charm.land/lipgloss/v2"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
@@ -96,6 +95,6 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
t.Row(v.Volume.Name, v.Volume.Driver, v.MachineName) t.Row(v.Volume.Name, v.Volume.Driver, v.MachineName)
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
+5 -3
View File
@@ -6,7 +6,6 @@ import (
"strings" "strings"
"time" "time"
"charm.land/lipgloss/v2"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
@@ -61,7 +60,10 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
if opts.machine != "" { if opts.machine != "" {
// Proxy requests to the specified machine. // Proxy requests to the specified machine.
ctx = client.ProxySingleMachineContext(ctx, opts.machine) ctx, _, err = client.ProxyMachinesContext(ctx, []string{opts.machine})
if err != nil {
return err
}
} }
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil) resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
@@ -134,6 +136,6 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
) )
} }
lipgloss.Println(t) fmt.Println(t)
return nil return nil
} }
-5
View File
@@ -1,5 +0,0 @@
_ _ _
_ _ _ __ ___| | ___ _ _ __| | __| |
| | | | '_ \ / __| |/ _ \| | | |/ _` |/ _` |
| |_| | | | | (__| | (_) | |_| | (_| | (_| |
\__,_|_| |_|\___|_|\___/ \__,_|\__,_|\__,_|
+3 -7
View File
@@ -7,11 +7,9 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/daemon" "github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/log" "github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/metrics"
"github.com/psviderski/uncloud/internal/version" "github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -26,7 +24,7 @@ func main() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uncloudd", Use: "uncloudd",
Short: "Uncloud machine daemon.", Short: "Uncloud machine daemon.",
Long: "Uncloud machine daemon.\n" + tui.URLStyle.Render(version.WebsiteURL), Version: version.String(),
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
@@ -41,13 +39,11 @@ func main() {
}, },
} }
cmd.PersistentFlags().StringVarP(&dataDir, "data-dir", "d", machine.DefaultDataDir, cmd.PersistentFlags().StringVarP(&dataDir, "data-dir", "d", machine.DefaultDataDir,
"Directory for storing persistent machine state.") "Directory for storing persistent machine state")
_ = cmd.MarkFlagDirname("data-dir") _ = cmd.MarkFlagDirname("data-dir")
// Add dial-stdio subcommand.
cmd.AddCommand(newDialStdioCommand()) cmd.AddCommand(newDialStdioCommand())
cmd.AddCommand(newVersionCommand())
metrics.Version.WithLabelValues(version.String()).Set(1)
// ctx is canceled when the daemon command is interrupted. // ctx is canceled when the daemon command is interrupted.
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
-82
View File
@@ -1,82 +0,0 @@
package main
import (
"bytes"
_ "embed"
"fmt"
"strings"
"text/template"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
//go:embed art.txt
var asciiArt string
// newVersionCommand creates a new command to print the version and build information for the binary.
func newVersionCommand() *cobra.Command {
var output string
cmd := &cobra.Command{
Use: "version",
Short: "Show version and build information.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
info := version.GetInfo()
w := cmd.OutOrStdout()
switch output {
case "":
fmt.Fprint(w, humanVersion(info))
case "json":
s, err := info.JSONString()
if err != nil {
return err
}
fmt.Fprintln(w, s)
default:
s, err := templateVersion(output, info)
if err != nil {
return err
}
fmt.Fprint(w, s)
}
return nil
},
}
cmd.Flags().StringVarP(&output, "output", "o", "",
"Output format: 'json' or a Go template (e.g., '{{.Version}}').\n"+
"Run with '-o json' to discover the field names available to the template.\n"+
"(default is human-readable)")
return cmd
}
func humanVersion(info version.Info) string {
var b strings.Builder
b.WriteString(asciiArt)
b.WriteString("\n")
b.WriteString(fmt.Sprintf("uncloudd: Uncloud machine daemon (%s)",
tui.URLStyle.Render(version.WebsiteURL)))
b.WriteString("\n\n")
b.WriteString(info.String())
return b.String()
}
// templateVersion renders the version info using the provided Go template.
func templateVersion(tmpl string, info version.Info) (string, error) {
t, err := template.New("version").Parse(tmpl)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err = t.Execute(&buf, info); err != nil {
return "", fmt.Errorf("execute template: %w", err)
}
return buf.String(), nil
}
+1 -1
View File
@@ -81,7 +81,7 @@ func TestDiscovery() error {
} }
endpoints[i] = &pb.Endpoint{ endpoints[i] = &pb.Endpoint{
Ip: ip, Ip: ip,
Port: network.DefaultWireGuardPort, Port: network.WireGuardPort,
} }
} }
if err = client.SetLocalData( if err = client.SetLocalData(
+10 -13
View File
@@ -11,11 +11,10 @@ require (
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/alecthomas/chroma/v2 v2.20.0 github.com/alecthomas/chroma/v2 v2.20.0
github.com/caarlos0/go-version v0.2.2
github.com/caddyserver/caddy/v2 v2.8.4 github.com/caddyserver/caddy/v2 v2.8.4
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/colorprofile v0.4.2
github.com/charmbracelet/x/ansi v0.11.7 github.com/charmbracelet/x/ansi v0.11.6
github.com/compose-spec/compose-go/v2 v2.9.0 github.com/compose-spec/compose-go/v2 v2.9.0
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/containerd/platforms v1.0.0-rc.1 github.com/containerd/platforms v1.0.0-rc.1
@@ -30,15 +29,12 @@ require (
github.com/goccy/go-yaml v1.17.1 github.com/goccy/go-yaml v1.17.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.2 github.com/google/go-containerregistry v0.20.2
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/miekg/dns v1.1.65 github.com/miekg/dns v1.1.65
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
github.com/prometheus/client_golang v1.22.0
github.com/psviderski/unregistry v0.4.1 github.com/psviderski/unregistry v0.4.1
github.com/siderolabs/grpc-proxy v0.5.1 github.com/siderolabs/grpc-proxy v0.5.1
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
@@ -48,8 +44,8 @@ require (
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0 golang.org/x/net v0.43.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.19.0
golang.org/x/sys v0.45.0 golang.org/x/sys v0.42.0
golang.org/x/term v0.34.0 golang.org/x/term v0.34.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@@ -98,7 +94,7 @@ require (
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
@@ -168,6 +164,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -202,12 +199,13 @@ require (
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/libdns/libdns v0.2.2 // indirect github.com/libdns/libdns v0.2.2 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
@@ -248,6 +246,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
@@ -358,5 +357,3 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
) )
replace charm.land/bubbletea/v2 => github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1
+16 -18
View File
@@ -1,5 +1,7 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98= charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
@@ -136,8 +138,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/caarlos0/go-version v0.2.2 h1:5r+nlrg4H2wOVwWjqRqRRIRbZ7ytRmjC9xoMIP0a5kQ=
github.com/caarlos0/go-version v0.2.2/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944=
github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw= github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw=
github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
@@ -155,12 +155,12 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
@@ -599,8 +599,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
@@ -621,8 +621,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -914,8 +914,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 h1:898LVw5QvmRX/y+pgzCo2pE/NbN9CZfdpiVnzAeAFDY=
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
@@ -1110,8 +1108,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1152,8 +1150,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+12 -46
View File
@@ -11,8 +11,6 @@ import (
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
@@ -176,8 +174,6 @@ type InitClusterOptions struct {
Version string Version string
AutoConfirm bool AutoConfirm bool
WireguardEndpoints []*pb.IPPort WireguardEndpoints []*pb.IPPort
WireguardMTU int
WireguardPort int
} }
// 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.
@@ -238,8 +234,6 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
MachineName: opts.MachineName, MachineName: opts.MachineName,
Network: pb.NewIPPrefix(opts.Network), Network: pb.NewIPPrefix(opts.Network),
WireguardEndpoints: opts.WireguardEndpoints, WireguardEndpoints: opts.WireguardEndpoints,
WireguardMtu: int32(opts.WireguardMTU),
WireguardPort: int32(opts.WireguardPort),
} }
if opts.PublicIP != nil { if opts.PublicIP != nil {
if opts.PublicIP.IsValid() { if opts.PublicIP.IsValid() {
@@ -323,15 +317,13 @@ type AddMachineOptions struct {
Version string Version string
AutoConfirm bool AutoConfirm bool
WireguardEndpoints []*pb.IPPort WireguardEndpoints []*pb.IPPort
WireguardMTU int
WireguardPort int
} }
// 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.
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the // The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
// cluster. The machine client is connected to the new machine and can be used to interact with it. // cluster. The machine client is connected to the new machine and can be used to interact with it.
// Both client should be closed after use by the caller. // Both client should be closed after use by the caller.
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *client.Client, _ *client.Client, err error) { func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
contextName := cli.ContextOverrideOrCurrent() contextName := cli.ContextOverrideOrCurrent()
c, err := cli.ConnectCluster(ctx) c, err := cli.ConnectCluster(ctx)
if err != nil { if err != nil {
@@ -352,19 +344,16 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
machineClient.Close() machineClient.Close()
} }
}() }()
// IMPORTANT: 'err' is a named return value so the deferred cleanups above observe it on every error return.
// Do not shadow it with ':=' in a nested scope, or the deferred client cleanup would be skipped on error.
// 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.
inspectResp, err := machineClient.MachineClient.InspectMachine(ctx, nil) // TODO: refactor to use client.InspectMachine.
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)
} }
minfo := inspectResp.Machines[0].Machine
if minfo.Id != "" { if minfo.Id != "" {
// Check if the machine is already a member of this cluster. // Check if the machine is already a member of this cluster.
var machines api.MachineMembersList machines, err := c.ListMachines(ctx, nil)
machines, err = c.ListMachines(ctx, nil)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("list cluster machines: %w", err) return nil, nil, fmt.Errorf("list cluster machines: %w", err)
} }
@@ -412,32 +401,11 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
} else { } else {
endpoints = make([]*pb.IPPort, len(token.Endpoints)) endpoints = make([]*pb.IPPort, len(token.Endpoints))
for i, addrPort := range token.Endpoints { for i, addrPort := range token.Endpoints {
// If a custom WireGuard port is specified, override the port from the token endpoints
// since the token was generated before the machine knows its configured port.
if opts.WireguardPort != 0 && opts.WireguardPort != network.DefaultWireGuardPort {
addrPort = netip.AddrPortFrom(addrPort.Addr(), uint16(opts.WireguardPort))
}
endpoints[i] = pb.NewIPPort(addrPort) endpoints[i] = pb.NewIPPort(addrPort)
} }
} }
// Default the machine name to the machine's hostname when not explicitly provided, ensuring it is
// unique within the cluster.
machineName := opts.MachineName
if machineName == "" {
var machines api.MachineMembersList
if machines, err = c.ListMachines(ctx, nil); err != nil {
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
}
existing := make([]string, len(machines))
for i, m := range machines {
existing[i] = m.Machine.Name
}
if machineName, err = cluster.DefaultMachineName(minfo.Hostname, existing); err != nil {
return nil, nil, fmt.Errorf("generate machine name: %w", err)
}
}
addReq := &pb.AddMachineRequest{ addReq := &pb.AddMachineRequest{
Name: machineName, Name: opts.MachineName,
Network: &pb.NetworkConfig{ Network: &pb.NetworkConfig{
Endpoints: endpoints, Endpoints: endpoints,
PublicKey: token.PublicKey, PublicKey: token.PublicKey,
@@ -457,16 +425,16 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
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)
} }
// Snapshot the cluster store version so the new machine can catch up before participating. // Get the current store DB version from the cluster to pass to the join request.
var storeVersion map[string]int64 var storeDBVersion int64
inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{}) inspectResp, err := c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
// TODO(lhf): remove Unimplemented check when v0.17.0 is released. // TODO(lhf): remove Unimplemented check when v0.17.0 is released.
if status.Convert(err).Code() != codes.Unimplemented { if status.Convert(err).Code() != codes.Unimplemented {
return nil, nil, fmt.Errorf("inspect current cluster machine: %w", err) return nil, nil, fmt.Errorf("inspect current cluster machine: %w", err)
} }
} else { } else {
storeVersion = inspectResp.Machines[0].StoreVersion 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.
@@ -483,11 +451,9 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
// Configure the remote machine to join the cluster. // Configure the remote machine to join the cluster.
joinReq := &pb.JoinClusterRequest{ joinReq := &pb.JoinClusterRequest{
Machine: addResp.Machine, Machine: addResp.Machine,
OtherMachines: otherMachines, OtherMachines: otherMachines,
MinStoreVersion: storeVersion, MinStoreDbVersion: storeDBVersion,
WireguardMtu: int32(opts.WireguardMTU),
WireguardPort: int32(opts.WireguardPort),
} }
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)
+5 -4
View File
@@ -31,9 +31,10 @@ func ConnectCluster(ctx context.Context, conn config.MachineConnection, opts Con
} }
// connectClusterWithProgress connects to the cluster while displaying a progress spinner. // connectClusterWithProgress connects to the cluster while displaying a progress spinner.
// If a terminal is not available, it falls back to simple progress logs to stderr. // If the stdout is not a terminal, it falls back to simple progress logs to stderr.
func connectClusterWithProgress(ctx context.Context, conn config.MachineConnection) (*client.Client, error) { func connectClusterWithProgress(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
if !tui.IsTerminalAvailable() { // If stdout is not a terminal, fall back to simple progress logs.
if !tui.IsStdoutTerminal() {
fmt.Fprintln(os.Stderr, "Connecting to", conn.String()) fmt.Fprintln(os.Stderr, "Connecting to", conn.String())
cli, err := connectCluster(ctx, conn) cli, err := connectCluster(ctx, conn)
if err != nil { if err != nil {
@@ -44,8 +45,8 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
return cli, err return cli, err
} }
// Run the connection TUI model. Render to stderr so stdout stays clean for command output. // Run the connection TUI model.
p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr)) p := tea.NewProgram(newConnectModel(ctx, conn))
model, err := p.Run() model, err := p.Run()
if err != nil { if err != nil {
return nil, fmt.Errorf("run connection TUI: %w", err) return nil, fmt.Errorf("run connection TUI: %w", err)
+5 -4
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -39,18 +40,18 @@ func BindEnvToFlag(cmd *cobra.Command, flagName, envVar string) {
} }
// ParseWireGuardEndpoints parses a list of endpoint strings into a list of IPPort protobuf messages. Each value can // ParseWireGuardEndpoints parses a list of endpoint strings into a list of IPPort protobuf messages. Each value can
// be an IP address, IP:PORT, IPv6, or [IPv6]:PORT. If the port is omitted, defaultPort is used. // be an IP address, IP:PORT, IPv6, or [IPv6]:PORT. If the port is omitted, the default WireGuard port is used.
func ParseWireGuardEndpoints(values []string, defaultPort uint16) ([]*pb.IPPort, error) { func ParseWireGuardEndpoints(values []string) ([]*pb.IPPort, error) {
endpoints := make([]*pb.IPPort, 0, len(values)) endpoints := make([]*pb.IPPort, 0, len(values))
for _, v := range values { for _, v := range values {
ap, err := netip.ParseAddrPort(v) ap, err := netip.ParseAddrPort(v)
if err != nil { if err != nil {
// Try parsing as a bare IP address and use the provided default port. // Try parsing as a bare IP address and use the default WireGuard port.
addr, addrErr := netip.ParseAddr(v) addr, addrErr := netip.ParseAddr(v)
if addrErr != nil { if addrErr != nil {
return nil, fmt.Errorf("invalid endpoint '%s': must be IP, IPv6, IP:PORT, or [IPv6]:PORT", v) return nil, fmt.Errorf("invalid endpoint '%s': must be IP, IPv6, IP:PORT, or [IPv6]:PORT", v)
} }
ap = netip.AddrPortFrom(addr, defaultPort) ap = netip.AddrPortFrom(addr, network.WireGuardPort)
} }
endpoints = append(endpoints, pb.NewIPPort(ap)) endpoints = append(endpoints, pb.NewIPPort(ap))
} }
+2 -2
View File
@@ -129,7 +129,7 @@ func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
output.WriteString(f.formatMachine(entry.Metadata.MachineName)) output.WriteString(f.formatMachine(entry.Metadata.MachineName))
output.WriteString(" ") output.WriteString(" ")
// Service/container_id or service name for a system service. // Service/container_id or service name for a systemd service.
output.WriteString(f.formatService(entry.Metadata.ServiceName, entry.Metadata.ContainerID, entry.Metadata.Hook)) output.WriteString(f.formatService(entry.Metadata.ServiceName, entry.Metadata.ContainerID, entry.Metadata.Hook))
output.WriteString(" ") output.WriteString(" ")
@@ -160,7 +160,7 @@ func (f *Formatter) printError(entry api.ServiceLogEntry) {
stringid.TruncateID(entry.Metadata.ContainerID), stringid.TruncateID(entry.Metadata.ContainerID),
entry.Metadata.MachineName) entry.Metadata.MachineName)
} else { } else {
msg = fmt.Sprintf("WARNING: log stream from system service '%s' on machine '%s'", msg = fmt.Sprintf("WARNING: log stream from systemd service '%s' on machine '%s'",
entry.Metadata.ServiceName, entry.Metadata.ServiceName,
entry.Metadata.MachineName) entry.Metadata.MachineName)
} }
+5 -7
View File
@@ -99,15 +99,15 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
} }
func promptResetMachine() error { func promptResetMachine() error {
if !tui.IsTerminalAvailable() { if !tui.IsStdinTerminal() {
return errors.New("the remote machine is already initialised as a cluster member; " + return errors.New("the remote machine is already initialised as a cluster member; " +
"cannot ask to confirm reset in non-interactive mode, " + "cannot ask to confirm reset in non-interactive mode, " +
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm") "use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
} }
fmt.Fprintln(os.Stderr, tui.Red.Render("The remote machine is already initialised as a cluster member. Resetting it will:\n"+ fmt.Println(tui.Red.Render("The remote machine is already initialised as a cluster member. Resetting it will:\n" +
"- Remove all service containers from the machine\n"+ "- Remove all service containers from the machine\n" +
"- Reset the Uncloud daemon on the machine to the uninitialised state")) "- Reset the machine to the uninitialised state"))
var confirm bool var confirm bool
form := huh.NewForm( form := huh.NewForm(
@@ -119,9 +119,7 @@ func promptResetMachine() error {
Value(&confirm), Value(&confirm),
), ),
).WithTheme(tui.ThemeConfirmDanger()). ).WithTheme(tui.ThemeConfirmDanger()).
WithAccessible(true). WithAccessible(true)
// Render to stderr so stdout stays clean for command output.
WithOutput(os.Stderr)
if err := form.Run(); err != nil { if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err) return fmt.Errorf("prompt user to confirm: %w", err)
} }
+1 -16
View File
@@ -25,9 +25,7 @@ func Confirm(title string) (bool, error) {
Value(&confirmed), Value(&confirmed),
), ),
).WithTheme(ThemeConfirm()). ).WithTheme(ThemeConfirm()).
WithAccessible(true). WithAccessible(true)
// Render to stderr so stdout stays clean for command output.
WithOutput(os.Stderr)
if err := form.Run(); err != nil { if err := form.Run(); err != nil {
return false, err return false, err
} }
@@ -53,14 +51,6 @@ func ThemeConfirmDanger() huh.Theme {
}) })
} }
// IsTerminalAvailable reports whether the control terminal (TTY) is available so an interactive TUI can run.
func IsTerminalAvailable() bool {
// Bubbletea interactive programs read keyboard input from stdin and render to stdout (default) or stderr (uncloud),
// so both must be terminals. In particular, bubbletea falls back to opening /dev/tty when stdin is not a terminal,
// which fails when there is no controlling terminal. See https://github.com/psviderski/uncloud/issues/386
return IsStdinTerminal() && IsStderrTerminal()
}
// IsStdinTerminal checks if the standard input is a terminal (TTY). // IsStdinTerminal checks if the standard input is a terminal (TTY).
func IsStdinTerminal() bool { func IsStdinTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd())) return term.IsTerminal(int(os.Stdin.Fd()))
@@ -71,11 +61,6 @@ func IsStdoutTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd())) return term.IsTerminal(int(os.Stdout.Fd()))
} }
// IsStderrTerminal checks if the standard error is a terminal (TTY).
func IsStderrTerminal() bool {
return term.IsTerminal(int(os.Stderr.Fd()))
}
// TerminalWidth returns the width of the terminal. // TerminalWidth returns the width of the terminal.
// Returns 0 if stdout is not a terminal or the width cannot be determined. // Returns 0 if stdout is not a terminal or the width cannot be determined.
func TerminalWidth() int { func TerminalWidth() int {
-37
View File
@@ -1,37 +0,0 @@
package tui
import (
"context"
"fmt"
"os"
"charm.land/huh/v2/spinner"
"charm.land/lipgloss/v2"
)
// RunSpinner shows an animated spinner that prints title while running action.
// When the terminal (TTY) is not available, it prints the title as plain text.
// It renders to stderr so stdout stays clean for command output.
func RunSpinner(ctx context.Context, title string, action func(ctx context.Context) error) error {
// Fall back to plain text when a terminal is not available to avoid the bubbletea /dev/tty error and to keep
// escape codes out of redirected output.
if !IsTerminalAvailable() {
fmt.Fprintln(os.Stderr, title)
return action(ctx)
}
return spinner.New().
// Leading space offsets the title from the spinner glyph.
Title(" " + title).
Type(spinner.MiniDot).
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
return &spinner.Styles{
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
Title: lipgloss.NewStyle(),
}
})).
WithOutput(os.Stderr).
Context(ctx).
ActionWithErr(action).
Run()
}
-1
View File
@@ -19,7 +19,6 @@ var (
BoldYellow = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Yellow) BoldYellow = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Yellow)
NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152")) NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152"))
URLStyle = lipgloss.NewStyle().Underline(true).Foreground(lipgloss.BrightBlue)
) )
// FormatImage renders an image reference with the given style, using a faint colon separator for tagged images. // FormatImage renders an image reference with the given style, using a faint colon separator for tagged images.
+31 -53
View File
@@ -19,10 +19,10 @@ import (
const ( const (
// http2ConnectTimeout is the maximum amount of time an HTTP2 client will wait for a connection to be established. // http2ConnectTimeout is the maximum amount of time an HTTP2 client will wait for a connection to be established.
http2ConnectTimeout = 3 * time.Second http2ConnectTimeout = 3 * time.Second
// http2MaxRetryTime bounds the transport retry of a single request. Kept short so it only absorbs transient // http2MaxRetryTime is the maximum amount of time an HTTP2 client will retry a request.
// blips. Retrying during a Corrosion outage is the job of the higher-level recovery loops. http2MaxRetryTime = 10 * time.Second
http2MaxRetryTime = 2 * time.Second // resubscribeMaxRetryTime is the maximum amount of time an API client will retry resubscribing to a query after
// resubscribeMaxRetryTime bounds resubscribing to a query after an error. // an error occurs.
resubscribeMaxRetryTime = 60 * time.Second resubscribeMaxRetryTime = 60 * time.Second
) )
@@ -33,43 +33,39 @@ type APIClient struct {
newResubBackoff func() backoff.BackOff newResubBackoff func() backoff.BackOff
} }
// NewAPIClient creates a new Corrosion API client. The bearerToken is sent in the Authorization header of every // NewAPIClient creates a new Corrosion API client. The client retries on network errors using an exponential backoff
// request to authenticate against Corrosion API. // policy with a maximum interval of 1 second and a maximum elapsed time of 10 seconds.
// // It automatically resubscribes to active subscriptions if an error occurs using an exponential backoff policy with a
// Retries are split by failure mode: the transport briefly retries a single request on transient network errors, while // maximum interval of 1 second and a maximum elapsed time of 60 seconds.
// subscriptions resubscribe from the last change ID when their stream breaks and own the wait while Corrosion is down.
//
// Use the WithHTTP2Client option to provide a custom HTTP client and the WithResubscribeBackoff option to change the // Use the WithHTTP2Client option to provide a custom HTTP client and the WithResubscribeBackoff option to change the
// backoff policy for resubscribing to a query. // backoff policy for resubscribing to a query.
func NewAPIClient(addr netip.AddrPort, bearerToken string, opts ...APIClientOption) (*APIClient, error) { func NewAPIClient(addr netip.AddrPort, opts ...APIClientOption) (*APIClient, error) {
baseURL, err := url.Parse(fmt.Sprintf("http://%s", addr)) baseURL, err := url.Parse(fmt.Sprintf("http://%s", addr))
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err) return nil, fmt.Errorf("invalid URL: %w", err)
} }
transport := &AuthRoundTripper{
Base: &RetryRoundTripper{
Base: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: http2ConnectTimeout,
}
return dialer.DialContext(ctx, network, addr)
},
},
NewBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxElapsedTime(http2MaxRetryTime),
)
},
},
Token: bearerToken,
}
c := &APIClient{ c := &APIClient{
baseURL: baseURL, baseURL: baseURL,
client: &http.Client{Transport: transport}, client: &http.Client{
Transport: &RetryRoundTripper{
Base: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: http2ConnectTimeout,
}
return dialer.DialContext(ctx, network, addr)
},
},
NewBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(http2MaxRetryTime),
)
},
},
},
newResubBackoff: func() backoff.BackOff { newResubBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff( return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond), backoff.WithInitialInterval(100*time.Millisecond),
@@ -87,8 +83,6 @@ func NewAPIClient(addr netip.AddrPort, bearerToken string, opts ...APIClientOpti
type APIClientOption func(*APIClient) type APIClientOption func(*APIClient)
// WithHTTP2Client replaces the client's HTTP transport. The provided client bypasses the built-in bearer-token
// injection, so the caller is responsible for setting the Authorization header.
func WithHTTP2Client(client *http.Client) APIClientOption { func WithHTTP2Client(client *http.Client) APIClientOption {
return func(c *APIClient) { return func(c *APIClient) {
c.client = client c.client = client
@@ -103,23 +97,6 @@ func WithResubscribeBackoff(newBackoff func() backoff.BackOff) APIClientOption {
} }
} }
// AuthRoundTripper sets the Authorization header on every outgoing request. An empty Token leaves the header untouched.
type AuthRoundTripper struct {
Base http.RoundTripper
Token string
}
func (rt *AuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.Token == "" {
return rt.Base.RoundTrip(req)
}
// RoundTripper contract: must not mutate the caller's request.
req = req.Clone(req.Context())
req.Header.Set("Authorization", "Bearer "+rt.Token)
return rt.Base.RoundTrip(req)
}
// RetryRoundTripper retries a single HTTP request on transient network errors using the backoff returned by NewBackoff.
type RetryRoundTripper struct { type RetryRoundTripper struct {
Base http.RoundTripper Base http.RoundTripper
// NewBackoff creates a new backoff policy for each request. // NewBackoff creates a new backoff policy for each request.
@@ -130,7 +107,8 @@ func (rt *RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
roundTrip := func() (*http.Response, error) { roundTrip := func() (*http.Response, error) {
resp, err := rt.Base.RoundTrip(req) resp, err := rt.Base.RoundTrip(req)
if err != nil { if err != nil {
if _, ok := errors.AsType[*net.OpError](err); ok { var opErr *net.OpError
if errors.As(err, &opErr) {
// Not certain, but I expect operational errors should generally be retryable. // Not certain, but I expect operational errors should generally be retryable.
slog.Debug("Retrying corrosion API request due to network error.", "error", err) slog.Debug("Retrying corrosion API request due to network error.", "error", err)
return nil, err return nil, err
-60
View File
@@ -1,60 +0,0 @@
package corrosion
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthRoundTripper_SetsAuthorizationHeader(t *testing.T) {
t.Parallel()
const token = "test-token-1234567890"
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
rt := &AuthRoundTripper{Base: http.DefaultTransport, Token: token}
client := &http.Client{Transport: rt}
req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, "Bearer "+token, gotAuth)
// Caller's request must not be mutated by the RoundTripper.
assert.Empty(t, req.Header.Get("Authorization"))
}
func TestAuthRoundTripper_EmptyTokenSkipsHeader(t *testing.T) {
t.Parallel()
var headerSeen bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, headerSeen = r.Header["Authorization"]
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
rt := &AuthRoundTripper{Base: http.DefaultTransport, Token: ""}
client := &http.Client{Transport: rt}
req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.False(t, headerSeen, "Authorization header should not be sent when token is empty")
}
+3 -47
View File
@@ -22,10 +22,6 @@ var (
ChangeTypeDelete ChangeType = "delete" ChangeTypeDelete ChangeType = "delete"
) )
// ErrSubscriptionNotFound is returned when resubscribing to a subscription that Corrosion
// no longer knows about (HTTP 404).
var ErrSubscriptionNotFound = errors.New("subscription not found")
type ChangeEvent struct { type ChangeEvent struct {
Type ChangeType Type ChangeType
RowID uint64 RowID uint64
@@ -290,25 +286,10 @@ func (c *APIClient) resubscribeWithBackoffFn(id string) func(context.Context, ui
return nil return nil
} }
return func(ctx context.Context, fromChange uint64) (*Subscription, error) { return func(ctx context.Context, fromChange uint64) (*Subscription, error) {
boff := backoff.WithContext(c.newResubBackoff(), ctx)
return backoff.RetryWithData(func() (*Subscription, error) { return backoff.RetryWithData(func() (*Subscription, error) {
sub, err := c.ResubscribeContext(ctx, id, fromChange) slog.Debug("Retrying to resubscribe to Corrosion query.", "id", id, "from_change", fromChange)
if err != nil { return c.ResubscribeContext(ctx, id, fromChange)
// A gone subscription can never be resubscribed, so stop retrying immediately and let the caller }, c.newResubBackoff())
// recover by creating a fresh subscription.
if errors.Is(err, ErrSubscriptionNotFound) {
slog.Error("Corrosion subscription no longer exists, giving up resubscribing.",
"id", id, "from_change", fromChange)
return nil, backoff.Permanent(fmt.Errorf("resubscribe to %s: %w", id, err))
}
// Don't log retries triggered by context cancellation, the backoff will stop immediately.
if ctx.Err() == nil {
slog.Error("Failed to resubscribe to Corrosion query. Retrying with backoff.",
"id", id, "from_change", fromChange, "err", err)
}
}
return sub, err
}, boff)
} }
} }
@@ -331,38 +312,13 @@ func (c *APIClient) ResubscribeContext(ctx context.Context, id string, fromChang
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
return nil, ErrSubscriptionNotFound
}
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("read response body: %w", err) return nil, fmt.Errorf("read response body: %w", err)
} }
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody) return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
} }
// Since https://github.com/superfly/corrosion/pull/355, Corrosion treats a resubscription from change 0 like
// a fresh subscription: it replays the full query snapshot (a columns event, all rows, and an end-of-query event)
// before streaming changes. We don't expose rows in this case, so drain the snapshot here before consuming changes.
if fromChange == 0 {
rows, err := newRows(ctx, resp.Body, false)
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("parse resubscribe response: %w", err)
}
// Drain the replayed rows until the end-of-query event to reach the change stream.
for rows.Next() {
}
if err = rows.Err(); err != nil {
resp.Body.Close()
return nil, fmt.Errorf("drain resubscribe snapshot: %w", err)
}
return newSubscription(ctx, id, nil, rows.body, rows.decoder, c.resubscribeWithBackoffFn(id)), nil
}
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
} }
-216
View File
@@ -1,216 +0,0 @@
package corrosion
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestClient builds an APIClient pointed at srv with a fast resubscribe backoff for tests.
func newTestClient(t *testing.T, srv *httptest.Server) *APIClient {
t.Helper()
baseURL, err := url.Parse(srv.URL)
require.NoError(t, err)
return &APIClient{
baseURL: baseURL,
client: srv.Client(),
newResubBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(time.Millisecond),
backoff.WithMaxInterval(10*time.Millisecond),
backoff.WithMaxElapsedTime(5*time.Second),
)
},
}
}
func flushString(t *testing.T, w http.ResponseWriter, s string) {
t.Helper()
_, err := w.Write([]byte(s))
require.NoError(t, err)
w.(http.Flusher).Flush()
}
// TestSubscription_ResubscribeFromZeroDrainsSnapshot verifies that when the connection drops before any
// change is seen (lastChangeID == 0) and Corrosion replays the full query snapshot on resubscription from
// change 0, the change handler drains the snapshot and delivers the subsequent change instead of looping on
// "expected change event, got: {Columns:...}".
// The change of behaviour in Corrosion: https://github.com/superfly/corrosion/pull/355
func TestSubscription_ResubscribeFromZeroDrainsSnapshot(t *testing.T) {
t.Parallel()
const snapshot = `{"columns":["id","info"]}
{"row":[1,["a","b"]]}
{"eoq":{"time":1e-7,"change_id":0}}
`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
// Initial subscription: send the snapshot, then end the stream to force a resubscribe.
w.Header().Set("corro-query-id", "test-sub")
w.WriteHeader(http.StatusOK)
flushString(t, w, snapshot)
case http.MethodGet:
// Resubscription from change 0: Corrosion v1.0.0+ replays the full snapshot, then changes.
require.Equal(t, "0", r.URL.Query().Get("from"))
w.Header().Set("corro-query-id", "test-sub")
w.WriteHeader(http.StatusOK)
flushString(t, w, snapshot+`{"change":["insert",2,["c","d"],1]}`+"\n")
// Keep the connection open so the handler doesn't end the stream and trigger another resubscribe.
<-r.Context().Done()
default:
require.FailNow(t, fmt.Sprintf("unexpected request method: %s", r.Method))
}
}))
defer srv.Close()
client := newTestClient(t, srv)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sub, err := client.SubscribeContext(ctx, "SELECT id, info FROM machines", nil, false)
require.NoError(t, err)
// Consume the initial rows so Changes can be called.
rows := sub.Rows()
for rows.Next() {
}
require.NoError(t, rows.Err())
changes, err := sub.Changes()
require.NoError(t, err)
select {
case change := <-changes:
require.NotNil(t, change, "changes channel closed with error: %v", sub.Err())
assert.Equal(t, ChangeTypeInsert, change.Type)
assert.Equal(t, uint64(2), change.RowID)
assert.Equal(t, uint64(1), change.ChangeID)
case <-time.After(5 * time.Second):
require.FailNow(t, "timed out waiting for a change after resubscribe", sub.Err())
}
}
// TestSubscription_ResubscribeFromNonZeroSkipsSnapshot verifies that a resubscription from a non-zero change
// streams changes directly, without a replayed snapshot, and the change handler delivers them.
func TestSubscription_ResubscribeFromNonZeroSkipsSnapshot(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
// Initial subscription with a non-zero last change id, then end the stream to force a resubscribe.
w.Header().Set("corro-query-id", "test-sub")
w.WriteHeader(http.StatusOK)
flushString(t, w, `{"columns":["id","info"]}`+"\n"+
`{"row":[1,["a","b"]]}`+"\n"+
`{"eoq":{"time":1e-7,"change_id":5}}`+"\n")
case http.MethodGet:
// Resubscription from change 5: only changes are streamed, no snapshot.
require.Equal(t, "5", r.URL.Query().Get("from"))
require.False(t, strings.Contains(r.URL.RawQuery, "skip_rows"))
w.WriteHeader(http.StatusOK)
flushString(t, w, `{"change":["update",1,["a","b2"],6]}`+"\n")
<-r.Context().Done()
default:
require.FailNow(t, fmt.Sprintf("unexpected request method: %s", r.Method))
}
}))
defer srv.Close()
client := newTestClient(t, srv)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sub, err := client.SubscribeContext(ctx, "SELECT id, info FROM machines", nil, false)
require.NoError(t, err)
rows := sub.Rows()
for rows.Next() {
}
require.NoError(t, rows.Err())
changes, err := sub.Changes()
require.NoError(t, err)
select {
case change := <-changes:
require.NotNil(t, change, "changes channel closed with error: %v", sub.Err())
assert.Equal(t, ChangeTypeUpdate, change.Type)
assert.Equal(t, uint64(6), change.ChangeID)
case <-time.After(5 * time.Second):
require.FailNow(t, "timed out waiting for a change after resubscribe", sub.Err())
}
}
// TestSubscription_ResubscribeNotFoundFailsFast verifies that when Corrosion returns 404 to a resubscription
// (the subscription no longer exists, e.g. after a restart that dropped it), the change handler stops retrying
// immediately and closes the changes channel with ErrSubscriptionNotFound, instead of retrying for the full
// backoff window.
func TestSubscription_ResubscribeNotFoundFailsFast(t *testing.T) {
t.Parallel()
var getCount atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
// Initial subscription, then end the stream to force a resubscribe.
w.Header().Set("corro-query-id", "test-sub")
w.WriteHeader(http.StatusOK)
flushString(t, w, `{"columns":["id","info"]}`+"\n"+
`{"row":[1,["a","b"]]}`+"\n"+
`{"eoq":{"time":1e-7,"change_id":5}}`+"\n")
case http.MethodGet:
// Resubscription: Corrosion no longer knows this subscription.
getCount.Add(1)
http.Error(w, "", http.StatusNotFound)
default:
require.FailNow(t, fmt.Sprintf("unexpected request method: %s", r.Method))
}
}))
defer srv.Close()
client := newTestClient(t, srv)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sub, err := client.SubscribeContext(ctx, "SELECT id, info FROM machines", nil, false)
require.NoError(t, err)
rows := sub.Rows()
for rows.Next() {
}
require.NoError(t, rows.Err())
changes, err := sub.Changes()
require.NoError(t, err)
select {
case change := <-changes:
// The channel must close (nil change) rather than deliver anything.
require.Nil(t, change, "expected the changes channel to close on a gone subscription")
case <-time.After(5 * time.Second):
require.FailNow(t, "timed out waiting for the changes channel to close")
}
require.ErrorIs(t, sub.Err(), ErrSubscriptionNotFound)
// The 404 must not be retried: exactly one GET resubscription request was made.
assert.Equal(t, int32(1), getCount.Load())
}
+2 -3
View File
@@ -31,9 +31,8 @@ const (
// //
// The two minimums are independent: a client might require a newer daemon for new // The two minimums are independent: a client might require a newer daemon for new
// features, while that same daemon could still handle requests from older clients. // features, while that same daemon could still handle requests from older clients.
// TODO: update to 0.20.0 before releasing 0.20.0. MinClientVersion = "0.0.0"
MinClientVersion = "0.20.0-nightly" MinServerVersion = "0.0.0"
MinServerVersion = "0.20.0-nightly"
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest" ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
) )
+20
View File
@@ -10,11 +10,31 @@ import (
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
) )
const (
UnitUncloud = "uncloud"
UnitDocker = "docker"
UnitCorrosion = "uncloud-corrosion"
)
func ValidUnit(unit string) bool {
switch unit {
case UnitUncloud:
case UnitDocker:
case UnitCorrosion:
default:
return false
}
return true
}
const journalctl = "journalctl" const journalctl = "journalctl"
var commandContext = exec.CommandContext // allow override for test var commandContext = exec.CommandContext // allow override for test
func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.ReadCloser, func() error, error) { func logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (io.ReadCloser, func() error, error) {
if !ValidUnit(unit) {
return nil, nil, fmt.Errorf("journal logs: invalid unit: %s", unit)
}
args := []string{"-u", unit, "--no-hostname"} args := []string{"-u", unit, "--no-hostname"}
args = append(args, "-n") args = append(args, "-n")
if opts.Tail > -1 { if opts.Tail > -1 {
+5
View File
@@ -3,6 +3,7 @@ package journal
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"slices" "slices"
"strconv" "strconv"
"time" "time"
@@ -12,6 +13,10 @@ import (
// Logs streams logs from a service and returns entries via a channel. // Logs streams logs from a service and returns entries via a channel.
func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan api.LogEntry, error) { func Logs(ctx context.Context, unit string, opts api.ServiceLogsOptions) (<-chan api.LogEntry, error) {
if !ValidUnit(unit) {
return nil, fmt.Errorf("journal logs: invalid unit: %s", unit)
}
reader, wait, err := logs(ctx, unit, opts) reader, wait, err := logs(ctx, unit, opts)
if err != nil { if err != nil {
return nil, err return nil, err
+296 -122
View File
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
// Deprecated: Use DNSRecord_RecordType.Descriptor instead. // Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) { func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9, 0} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11, 0}
} }
type AddMachineRequest struct { type AddMachineRequest struct {
@@ -339,6 +339,126 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
return nil return nil
} }
type UpdateMachineRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Machine to update
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
// Updated machine information
Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"`
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof" json:"public_ip,omitempty"`
Endpoints []*IPPort `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
}
func (x *UpdateMachineRequest) Reset() {
*x = UpdateMachineRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpdateMachineRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateMachineRequest) ProtoMessage() {}
func (x *UpdateMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
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 UpdateMachineRequest.ProtoReflect.Descriptor instead.
func (*UpdateMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
}
func (x *UpdateMachineRequest) GetMachineId() string {
if x != nil {
return x.MachineId
}
return ""
}
func (x *UpdateMachineRequest) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *UpdateMachineRequest) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
func (x *UpdateMachineRequest) GetEndpoints() []*IPPort {
if x != nil {
return x.Endpoints
}
return nil
}
type UpdateMachineResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
}
func (x *UpdateMachineResponse) Reset() {
*x = UpdateMachineResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpdateMachineResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateMachineResponse) ProtoMessage() {}
func (x *UpdateMachineResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
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 UpdateMachineResponse.ProtoReflect.Descriptor instead.
func (*UpdateMachineResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
}
func (x *UpdateMachineResponse) GetMachine() *MachineInfo {
if x != nil {
return x.Machine
}
return nil
}
type RemoveMachineRequest struct { type RemoveMachineRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -350,7 +470,7 @@ type RemoveMachineRequest struct {
func (x *RemoveMachineRequest) Reset() { func (x *RemoveMachineRequest) Reset() {
*x = RemoveMachineRequest{} *x = RemoveMachineRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -363,7 +483,7 @@ func (x *RemoveMachineRequest) String() string {
func (*RemoveMachineRequest) ProtoMessage() {} func (*RemoveMachineRequest) ProtoMessage() {}
func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message { func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
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 {
@@ -376,7 +496,7 @@ func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead.
func (*RemoveMachineRequest) Descriptor() ([]byte, []int) { func (*RemoveMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
} }
func (x *RemoveMachineRequest) GetId() string { func (x *RemoveMachineRequest) GetId() string {
@@ -397,7 +517,7 @@ type Domain struct {
func (x *Domain) Reset() { func (x *Domain) Reset() {
*x = Domain{} *x = Domain{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -410,7 +530,7 @@ func (x *Domain) String() string {
func (*Domain) ProtoMessage() {} func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message { func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
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 {
@@ -423,7 +543,7 @@ func (x *Domain) ProtoReflect() protoreflect.Message {
// Deprecated: Use Domain.ProtoReflect.Descriptor instead. // Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) { func (*Domain) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
} }
func (x *Domain) GetName() string { func (x *Domain) GetName() string {
@@ -444,7 +564,7 @@ type ReserveDomainRequest struct {
func (x *ReserveDomainRequest) Reset() { func (x *ReserveDomainRequest) Reset() {
*x = ReserveDomainRequest{} *x = ReserveDomainRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -457,7 +577,7 @@ func (x *ReserveDomainRequest) String() string {
func (*ReserveDomainRequest) ProtoMessage() {} func (*ReserveDomainRequest) ProtoMessage() {}
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message { func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6] mi := &file_internal_machine_api_pb_cluster_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 {
@@ -470,7 +590,7 @@ func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) { func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
} }
func (x *ReserveDomainRequest) GetEndpoint() string { func (x *ReserveDomainRequest) GetEndpoint() string {
@@ -491,7 +611,7 @@ type CreateDomainRecordsRequest struct {
func (x *CreateDomainRecordsRequest) Reset() { func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{} *x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -504,7 +624,7 @@ func (x *CreateDomainRecordsRequest) String() string {
func (*CreateDomainRecordsRequest) ProtoMessage() {} func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7] mi := &file_internal_machine_api_pb_cluster_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 {
@@ -517,7 +637,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
} }
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
@@ -538,7 +658,7 @@ type CreateDomainRecordsResponse struct {
func (x *CreateDomainRecordsResponse) Reset() { func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{} *x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -551,7 +671,7 @@ func (x *CreateDomainRecordsResponse) String() string {
func (*CreateDomainRecordsResponse) ProtoMessage() {} func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8] mi := &file_internal_machine_api_pb_cluster_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 {
@@ -564,7 +684,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{10}
} }
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
@@ -587,7 +707,7 @@ type DNSRecord struct {
func (x *DNSRecord) Reset() { func (x *DNSRecord) Reset() {
*x = DNSRecord{} *x = DNSRecord{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9] mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -600,7 +720,7 @@ func (x *DNSRecord) String() string {
func (*DNSRecord) ProtoMessage() {} func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message { func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9] mi := &file_internal_machine_api_pb_cluster_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 {
@@ -613,7 +733,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead. // Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) { func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11}
} }
func (x *DNSRecord) GetName() string { func (x *DNSRecord) GetName() string {
@@ -677,66 +797,87 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x69, 0x6e, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x04,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61,
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x50, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01,
0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20,
0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f,
0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x69, 0x70, 0x22, 0x43, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xca, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x92, 0x04, 0x0a, 0x07,
0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30,
0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 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, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
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, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 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 (
@@ -752,7 +893,7 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
} }
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState (MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType (DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
@@ -760,46 +901,54 @@ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse (*AddMachineResponse)(nil), // 3: api.AddMachineResponse
(*MachineMember)(nil), // 4: api.MachineMember (*MachineMember)(nil), // 4: api.MachineMember
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse (*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest (*UpdateMachineRequest)(nil), // 6: api.UpdateMachineRequest
(*Domain)(nil), // 7: api.Domain (*UpdateMachineResponse)(nil), // 7: api.UpdateMachineResponse
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest (*RemoveMachineRequest)(nil), // 8: api.RemoveMachineRequest
(*CreateDomainRecordsRequest)(nil), // 9: api.CreateDomainRecordsRequest (*Domain)(nil), // 9: api.Domain
(*CreateDomainRecordsResponse)(nil), // 10: api.CreateDomainRecordsResponse (*ReserveDomainRequest)(nil), // 10: api.ReserveDomainRequest
(*DNSRecord)(nil), // 11: api.DNSRecord (*CreateDomainRecordsRequest)(nil), // 11: api.CreateDomainRecordsRequest
(*NetworkConfig)(nil), // 12: api.NetworkConfig (*CreateDomainRecordsResponse)(nil), // 12: api.CreateDomainRecordsResponse
(*IP)(nil), // 13: api.IP (*DNSRecord)(nil), // 13: api.DNSRecord
(*MachineInfo)(nil), // 14: api.MachineInfo (*NetworkConfig)(nil), // 14: api.NetworkConfig
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty (*IP)(nil), // 15: api.IP
(*MachineInfo)(nil), // 16: api.MachineInfo
(*IPPort)(nil), // 17: api.IPPort
(*emptypb.Empty)(nil), // 18: google.protobuf.Empty
} }
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{ var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
12, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig 14, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
13, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP 15, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
14, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo 16, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
14, // 3: api.MachineMember.machine:type_name -> api.MachineInfo 16, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState 0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember 4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
11, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord 15, // 6: api.UpdateMachineRequest.public_ip:type_name -> api.IP
11, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord 17, // 7: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType 16, // 8: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest 13, // 9: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
15, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty 13, // 10: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest 1, // 11: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest 2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
15, // 13: api.Cluster.GetDomain:input_type -> google.protobuf.Empty 18, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
15, // 14: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty 6, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
9, // 15: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest 8, // 15: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
3, // 16: api.Cluster.AddMachine:output_type -> api.AddMachineResponse 10, // 16: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
5, // 17: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse 18, // 17: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
15, // 18: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty 18, // 18: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
7, // 19: api.Cluster.ReserveDomain:output_type -> api.Domain 11, // 19: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
7, // 20: api.Cluster.GetDomain:output_type -> api.Domain 3, // 20: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
7, // 21: api.Cluster.ReleaseDomain:output_type -> api.Domain 5, // 21: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
10, // 22: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse 7, // 22: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
16, // [16:23] is the sub-list for method output_type 18, // 23: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
9, // [9:16] is the sub-list for method input_type 9, // 24: api.Cluster.ReserveDomain:output_type -> api.Domain
9, // [9:9] is the sub-list for extension type_name 9, // 25: api.Cluster.GetDomain:output_type -> api.Domain
9, // [9:9] is the sub-list for extension extendee 9, // 26: api.Cluster.ReleaseDomain:output_type -> api.Domain
0, // [0:9] is the sub-list for field type_name 12, // 27: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
20, // [20:28] is the sub-list for method output_type
12, // [12:20] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_cluster_proto_init() } func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -859,7 +1008,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*RemoveMachineRequest); i { switch v := v.(*UpdateMachineRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -871,7 +1020,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*Domain); i { switch v := v.(*UpdateMachineResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -883,7 +1032,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); i { switch v := v.(*RemoveMachineRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -895,7 +1044,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i { switch v := v.(*Domain); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -907,7 +1056,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i { switch v := v.(*ReserveDomainRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -919,6 +1068,30 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i { switch v := v.(*DNSRecord); i {
case 0: case 0:
return &v.state return &v.state
@@ -931,13 +1104,14 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 2, NumEnums: 2,
NumMessages: 10, NumMessages: 12,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+15
View File
@@ -11,6 +11,7 @@ import "internal/machine/api/pb/machine.proto";
service Cluster { service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse); rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse); rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty); rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain); rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
@@ -50,6 +51,20 @@ message ListMachinesResponse {
repeated MachineMember machines = 1; repeated MachineMember machines = 1;
} }
message UpdateMachineRequest {
// Machine to update
string machine_id = 1;
// Updated machine information
optional string name = 2;
optional IP public_ip = 3;
repeated IPPort endpoints = 4;
}
message UpdateMachineResponse {
MachineInfo machine = 1;
}
message RemoveMachineRequest { message RemoveMachineRequest {
string id = 1; string id = 1;
} }
@@ -22,6 +22,7 @@ const _ = grpc.SupportPackageIsVersion9
const ( const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine" Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines" Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_UpdateMachine_FullMethodName = "/api.Cluster/UpdateMachine"
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine" Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain" Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain" Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
@@ -35,6 +36,7 @@ const (
type ClusterClient interface { type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error) AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
@@ -70,6 +72,16 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
return out, nil return out, nil
} }
func (c *clusterClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMachineResponse)
err := c.cc.Invoke(ctx, Cluster_UpdateMachine_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { func (c *clusterClient) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, 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)
@@ -126,6 +138,7 @@ func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomai
type ClusterServer interface { type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error) AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error) GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
@@ -147,6 +160,9 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) { func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
} }
func (UnimplementedClusterServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
}
func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) { func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveMachine not implemented") return nil, status.Errorf(codes.Unimplemented, "method RemoveMachine not implemented")
} }
@@ -219,6 +235,24 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Cluster_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).UpdateMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_UpdateMachine_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).UpdateMachine(ctx, req.(*UpdateMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_RemoveMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Cluster_RemoveMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveMachineRequest) in := new(RemoveMachineRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -324,6 +358,10 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines", MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler, Handler: _Cluster_ListMachines_Handler,
}, },
{
MethodName: "UpdateMachine",
Handler: _Cluster_UpdateMachine_Handler,
},
{ {
MethodName: "RemoveMachine", MethodName: "RemoveMachine",
Handler: _Cluster_RemoveMachine_Handler, Handler: _Cluster_RemoveMachine_Handler,
+50 -73
View File
@@ -81,12 +81,8 @@ type Metadata struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
// ID of the machine the response came from.
MachineId string `protobuf:"bytes,4,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
// Name of the machine the response came from.
MachineName string `protobuf:"bytes,5,opt,name=machine_name,json=machineName,proto3" json:"machine_name,omitempty"`
// Address of the machine the response came from. // Address of the machine the response came from.
MachineAddr string `protobuf:"bytes,1,opt,name=machine_addr,json=machineAddr,proto3" json:"machine_addr,omitempty"` Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
// error is set if the request to upstream failed. The rest of the response is undefined. // error is set if the request to upstream failed. The rest of the response is undefined.
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
// error as a gRPC Status message. // error as a gRPC Status message.
@@ -125,23 +121,9 @@ func (*Metadata) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0} return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
} }
func (x *Metadata) GetMachineId() string { func (x *Metadata) GetMachine() string {
if x != nil { if x != nil {
return x.MachineId return x.Machine
}
return ""
}
func (x *Metadata) GetMachineName() string {
if x != nil {
return x.MachineName
}
return ""
}
func (x *Metadata) GetMachineAddr() string {
if x != nil {
return x.MachineAddr
} }
return "" return ""
} }
@@ -567,58 +549,53 @@ var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb1, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53,
0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a,
0x69, 0x6e, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x61, 0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50,
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70,
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65,
0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x35, 0x0a, 0x06, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04,
0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73,
0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x22, 0x75, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x22, 0x75, 0x0a, 0x0b, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73,
0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45,
0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01,
0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06,
0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10,
0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x09, 0x48, 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 0x42, 0x37, 0x5a, 0x35,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x48, 0x45, 0x41, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 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 (
+1 -6
View File
@@ -11,13 +11,8 @@ import "google/protobuf/timestamp.proto";
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information // Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request. // about the machine that responded to the request.
message Metadata { message Metadata {
// ID of the machine the response came from.
string machine_id = 4;
// Name of the machine the response came from.
string machine_name = 5;
// Address of the machine the response came from. // Address of the machine the response came from.
string machine_addr = 1; string machine = 1;
// error is set if the request to upstream failed. The rest of the response is undefined. // error is set if the request to upstream failed. The rest of the response is undefined.
string error = 2; string error = 2;
// error as a gRPC Status message. // error as a gRPC Status message.
File diff suppressed because it is too large Load Diff
+4 -40
View File
@@ -19,8 +19,6 @@ service Machine {
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. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse); rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
// UpdateMachine updates the configuration of the machine.
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
rpc InspectWireGuardNetwork(google.protobuf.Empty) returns (InspectWireGuardNetworkResponse); 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.
@@ -36,19 +34,6 @@ message MachineInfo {
string name = 2; string name = 2;
NetworkConfig network = 3; NetworkConfig network = 3;
IP public_ip = 4; IP public_ip = 4;
// Version of the machine daemon (uncloudd).
string daemon_version = 7;
// Version of the Docker engine running on the machine. Could be empty if unable to get.
string docker_version = 8;
// Operating system hostname.
string hostname = 5;
// CPU architecture.
string arch = 6;
// Human-readable operating system name and version, e.g. "Ubuntu 24.04.4 LTS". Empty if cannot be determined.
string os_pretty_name = 9;
// Kernel release version, e.g. "6.8.0-31-generic".
string kernel_version = 10;
} }
message NetworkConfig { message NetworkConfig {
@@ -58,17 +43,6 @@ message NetworkConfig {
bytes public_key = 4; bytes public_key = 4;
} }
message UpdateMachineRequest {
// Updated machine information. Only the set fields are applied.
optional string name = 1;
optional IP public_ip = 2;
repeated IPPort endpoints = 3;
}
message UpdateMachineResponse {
MachineInfo machine = 1;
}
message CheckPrerequisitesResponse { message CheckPrerequisitesResponse {
// Overall status of the checks. // Overall status of the checks.
bool satisfied = 1; bool satisfied = 1;
@@ -87,10 +61,6 @@ message InitClusterRequest {
// Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones. // Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones.
repeated IPPort wireguard_endpoints = 5; repeated IPPort wireguard_endpoints = 5;
// WireGuard listen port for this machine. Uses the default port (51820) if 0 or not set.
int32 wireguard_port = 6;
// MTU of the WireGuard interface on this machine. The daemon auto-detects the optimal MTU if 0 or not set.
int32 wireguard_mtu = 7;
} }
message InitClusterResponse { message InitClusterResponse {
@@ -100,13 +70,8 @@ message InitClusterResponse {
message JoinClusterRequest { message JoinClusterRequest {
MachineInfo machine = 1; MachineInfo machine = 1;
repeated MachineInfo other_machines = 3; repeated MachineInfo other_machines = 3;
// WireGuard listen port for this machine. Uses the default port (51820) if 0 or not set. // Minimum store database version the new machine should sync to before starting cluster operations.
int32 wireguard_port = 5; int64 min_store_db_version = 4;
// MTU of the WireGuard interface on this machine. The daemon auto-detects the optimal MTU if 0 or not set.
int32 wireguard_mtu = 7;
// Cluster store version this machine must reach before participating.
// Per-actor vector (Corrosion actor UUID → max applied db_version).
map<string, int64> min_store_version = 6;
} }
message InspectMachineResponse { message InspectMachineResponse {
@@ -117,11 +82,10 @@ message InspectMachineResponse {
message MachineDetails { message MachineDetails {
Metadata metadata = 1; Metadata metadata = 1;
MachineInfo machine = 2; MachineInfo machine = 2;
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
int64 store_db_version = 3;
// Round-trip times to other machines in the cluster, keyed by peer machine ID. // Round-trip times to other machines in the cluster, keyed by peer machine ID.
map<string, RTTStats> rtts = 4; map<string, RTTStats> rtts = 4;
// Current cluster store version observed on this machine.
// Per-actor vector (Corrosion actor UUID → max applied db_version) read from Corrosion crsql_db_versions.
map<string, int64> store_version = 5;
} }
message TokenResponse { message TokenResponse {
@@ -26,7 +26,6 @@ const (
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_InspectMachine_FullMethodName = "/api.Machine/InspectMachine"
Machine_UpdateMachine_FullMethodName = "/api.Machine/UpdateMachine"
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork" 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"
@@ -46,8 +45,6 @@ type MachineClient interface {
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 retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
// UpdateMachine updates the configuration of the machine.
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error) 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.
@@ -124,16 +121,6 @@ func (c *machineClient) InspectMachine(ctx context.Context, in *emptypb.Empty, o
return out, nil return out, nil
} }
func (c *machineClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMachineResponse)
err := c.cc.Invoke(ctx, Machine_UpdateMachine_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) { func (c *machineClient) InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectWireGuardNetworkResponse) out := new(InspectWireGuardNetworkResponse)
@@ -196,8 +183,6 @@ type MachineServer interface {
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 retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
// UpdateMachine updates the configuration of the machine.
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) 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.
@@ -232,9 +217,6 @@ func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*Mac
func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) { func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented") return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented")
} }
func (UnimplementedMachineServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
}
func (UnimplementedMachineServer) InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) { func (UnimplementedMachineServer) InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectWireGuardNetwork not implemented") return nil, status.Errorf(codes.Unimplemented, "method InspectWireGuardNetwork not implemented")
} }
@@ -376,24 +358,6 @@ func _Machine_InspectMachine_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Machine_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).UpdateMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_UpdateMachine_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).UpdateMachine(ctx, req.(*UpdateMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InspectWireGuardNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Machine_InspectWireGuardNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty) in := new(emptypb.Empty)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -490,10 +454,6 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectMachine", MethodName: "InspectMachine",
Handler: _Machine_InspectMachine_Handler, Handler: _Machine_InspectMachine_Handler,
}, },
{
MethodName: "UpdateMachine",
Handler: _Machine_UpdateMachine_Handler,
},
{ {
MethodName: "InspectWireGuardNetwork", MethodName: "InspectWireGuardNetwork",
Handler: _Machine_InspectWireGuardNetwork_Handler, Handler: _Machine_InspectWireGuardNetwork_Handler,
+10 -17
View File
@@ -4,18 +4,15 @@ import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
// MetadataBackend wraps a proxy.Backend and injects machine metadata into responses in One2Many mode. // One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
type MetadataBackend struct { // successful and failure responses might be returned in One2Many mode.
proxy.Backend type One2ManyResponder struct {
MachineID string machine string
MachineName string
MachineAddr string
} }
// AppendInfo is called to enhance response from the backend with additional data. // AppendInfo is called to enhance response from the backend with additional data.
@@ -62,12 +59,10 @@ type MetadataBackend struct {
// cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf, // cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf,
// which builds 'common.Metadata' field, append it to original response message, build new header // which builds 'common.Metadata' field, append it to original response message, build new header
// for new length of some response, and add back new field header. // for new length of some response, and add back new field header.
func (b *MetadataBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) { func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
payload, err := proto.Marshal(&pb.Empty{ payload, err := proto.Marshal(&pb.Empty{
Metadata: &pb.Metadata{ Metadata: &pb.Metadata{
MachineAddr: b.MachineAddr, Machine: b.machine,
MachineId: b.MachineID,
MachineName: b.MachineName,
}, },
}) })
@@ -129,14 +124,12 @@ func (b *MetadataBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error
// //
// Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse // Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse
// message. // message.
func (b *MetadataBackend) BuildError(streaming bool, err error) ([]byte, error) { func (b *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
var resp proto.Message = &pb.Empty{ var resp proto.Message = &pb.Empty{
Metadata: &pb.Metadata{ Metadata: &pb.Metadata{
MachineAddr: b.MachineAddr, Machine: b.machine,
MachineId: b.MachineID, Error: err.Error(),
MachineName: b.MachineName, Status: status.Convert(err).Proto(),
Error: err.Error(),
Status: status.Convert(err).Proto(),
}, },
} }
+31 -80
View File
@@ -2,10 +2,7 @@ package proxy
import ( import (
"context" "context"
"errors"
"fmt"
"sync" "sync"
"sync/atomic"
"github.com/siderolabs/grpc-proxy/proxy" "github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -18,22 +15,27 @@ type Director struct {
localBackend *LocalBackend localBackend *LocalBackend
remotePort uint16 remotePort uint16
remoteBackends sync.Map remoteBackends sync.Map
localAddress atomic.Value // mu synchronizes access to localAddress.
mapper MachineMapper mu sync.RWMutex
localAddress string
} }
func NewDirector(localSockPath string, remotePort uint16, mapper MachineMapper) *Director { func NewDirector(localSockPath string, remotePort uint16) *Director {
return &Director{ return &Director{
localBackend: NewLocalBackend(localSockPath), localBackend: NewLocalBackend(localSockPath, ""),
remotePort: remotePort, remotePort: remotePort,
mapper: mapper,
} }
} }
// UpdateLocalAddress updates the local machine address used to identify which requests should be proxied // UpdateLocalAddress updates the local machine address used to identify which requests should be proxied
// to the local gRPC server. It is called once during machine startup before the proxy server accepts requests. // to the local gRPC server.
func (d *Director) UpdateLocalAddress(addr string) { func (d *Director) UpdateLocalAddress(addr string) {
d.localAddress.Store(addr) d.mu.Lock()
defer d.mu.Unlock()
d.localAddress = addr
// Replace the local backend with the one that has local address set.
d.localBackend = NewLocalBackend(d.localBackend.sockPath, addr)
} }
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based // Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
@@ -49,90 +51,39 @@ func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.M
return proxy.One2One, []proxy.Backend{d.localBackend}, nil return proxy.One2One, []proxy.Backend{d.localBackend}, nil
} }
// If the request metadata doesn't contain machines to proxy to, send it to the local backend. // If the request metadata doesn't contain machines to proxy to, send it to the local backend.
machines, hasMachines := md["machines"] machines, ok := md["machines"]
machine, hasMachine := md["machine"] if !ok {
if !hasMachines && !hasMachine {
return proxy.One2One, []proxy.Backend{d.localBackend}, nil return proxy.One2One, []proxy.Backend{d.localBackend}, nil
} }
// Handle singular "machine" case (One2One, no metadata injection)
if hasMachine {
if len(machine) != 1 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
"proxy metadata 'machine' must have exactly one value")
}
if hasMachines {
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
"both 'machine' and 'machines' proxy metadata are set")
}
targets, err := d.mapper.MapMachines(ctx, machine)
if err != nil {
return proxy.One2One, nil, mapErrorToStatus(err)
}
backend, err := d.getBackend(targets[0].Addr)
if err != nil {
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
}
// For One2One, we don't wrap in MetadataBackend as we don't inject metadata.
return proxy.One2One, []proxy.Backend{backend}, nil
}
// Handle plural "machines" case (One2Many, always metadata injection)
if len(machines) == 0 { if len(machines) == 0 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "proxy metadata 'machines' is empty") return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
} }
targets, err := d.mapper.MapMachines(ctx, machines) d.mu.RLock()
if err != nil { localAddress := d.localAddress
return proxy.One2One, nil, mapErrorToStatus(err) localBackend := d.localBackend
} d.mu.RUnlock()
backends := make([]proxy.Backend, len(targets)) backends := make([]proxy.Backend, len(machines))
for i, t := range targets { for i, addr := range machines {
backend, err := d.getBackend(t.Addr) if addr == localAddress {
backends[i] = localBackend
continue
}
backend, err := d.remoteBackend(addr)
if err != nil { if err != nil {
return proxy.One2One, nil, status.Error(codes.Internal, err.Error()) return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
} }
backends[i] = backend
// Wrap with metadata injector
backends[i] = &MetadataBackend{
Backend: backend,
MachineID: t.ID,
MachineName: t.Name,
MachineAddr: t.Addr,
}
} }
// TODO: should we periodically close and delete outdated remote backends (the ones left after removing machines)? if len(backends) == 1 {
// IIRC the proxy will try to reconnect to them indefinitely. This can be stopped by restarting the daemon. return proxy.One2One, backends, nil
// But we can clean them up, e.g. when a client requests 'machines: *' so we know all the current targets }
// or run a background goroutine that periodically lists them and closes old remoteBackends.
return proxy.One2Many, backends, nil return proxy.One2Many, backends, nil
} }
// mapErrorToStatus converts mapper errors to appropriate gRPC status errors.
func mapErrorToStatus(err error) error {
if notFound, ok := errors.AsType[*MachinesNotFoundError](err); ok {
return status.Error(codes.InvalidArgument, notFound.Error())
}
// Check if already a gRPC status error.
if _, ok := status.FromError(err); ok {
return err
}
return status.Error(codes.Internal, fmt.Sprintf("failed to resolve machines: %v", err))
}
// getBackend returns a backend for the given address, utilizing local backend if matching local address.
func (d *Director) getBackend(addr string) (proxy.Backend, error) {
if localAddr, _ := d.localAddress.Load().(string); localAddr != "" && addr == localAddr {
return d.localBackend, nil
}
return d.remoteBackend(addr)
}
// remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one. // remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one.
func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) { func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
b, ok := d.remoteBackends.Load(addr) b, ok := d.remoteBackends.Load(addr)
-255
View File
@@ -1,255 +0,0 @@
package proxy
import (
"context"
"errors"
"testing"
"github.com/siderolabs/grpc-proxy/proxy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type mockMapper struct {
targets []MachineTarget
err error
}
func (m *mockMapper) MapMachines(_ context.Context, _ []string) ([]MachineTarget, error) {
if m.err != nil {
return nil, m.err
}
return m.targets, nil
}
func TestDirector_Director(t *testing.T) {
d := NewDirector("/tmp/test.sock", 8080, nil)
t.Cleanup(d.Close)
// Use a valid IPv6 address for remote targets.
remoteTarget := MachineTarget{ID: "id-2", Name: "machine-b", Addr: "fd00::2"}
localTarget := MachineTarget{ID: "id-1", Name: "machine-a", Addr: "fd00::1"}
t.Run("no metadata routes to local", func(t *testing.T) {
ctx := context.Background()
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("proxy-authority routes to local", func(t *testing.T) {
md := metadata.Pairs("proxy-authority", "test", "machines", remoteTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("machine singular local", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}
md := metadata.New(map[string]string{"machine": localTarget.Name})
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("machine singular remote", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{remoteTarget}}
md := metadata.New(map[string]string{"machine": remoteTarget.Name})
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*RemoteBackend)(nil), backends[0])
assert.Equal(t, "[fd00::2]:8080", backends[0].(*RemoteBackend).target)
})
t.Run("machine not found", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}
md := metadata.New(map[string]string{"machine": "missing"})
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "machine not found: missing")
})
t.Run("machines plural single local", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}
md := metadata.Pairs("machines", localTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2Many, mode)
assert.Len(t, backends, 1)
mb := backends[0].(*MetadataBackend)
assert.Equal(t, localTarget.ID, mb.MachineID)
assert.Equal(t, localTarget.Name, mb.MachineName)
assert.Equal(t, localTarget.Addr, mb.MachineAddr)
assert.IsType(t, (*LocalBackend)(nil), mb.Backend)
})
t.Run("machines plural multiple", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget, remoteTarget}}
md := metadata.Pairs("machines", localTarget.Name, "machines", remoteTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2Many, mode)
assert.Len(t, backends, 2)
// First backend should be local.
mb0 := backends[0].(*MetadataBackend)
assert.Equal(t, localTarget.ID, mb0.MachineID)
assert.IsType(t, (*LocalBackend)(nil), mb0.Backend)
// Second backend should be remote.
mb1 := backends[1].(*MetadataBackend)
assert.Equal(t, remoteTarget.ID, mb1.MachineID)
assert.IsType(t, (*RemoteBackend)(nil), mb1.Backend)
assert.Equal(t, "[fd00::2]:8080", mb1.Backend.(*RemoteBackend).target)
})
t.Run("machines empty string", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{""}}}
md := metadata.Pairs("machines", "")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "machine not found")
})
t.Run("machine empty slice", func(t *testing.T) {
md := metadata.MD{"machine": []string{}}
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "proxy metadata 'machine' must have exactly one value")
})
t.Run("machines empty slice", func(t *testing.T) {
md := metadata.MD{"machines": []string{}}
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "proxy metadata 'machines' is empty")
})
t.Run("both machine and machines set", func(t *testing.T) {
md := metadata.Pairs("machine", "m1", "machines", "m1", "machines", "m2")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "both 'machine' and 'machines' proxy metadata are set")
})
t.Run("machines not found", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}
md := metadata.Pairs("machines", "missing")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
})
t.Run("machines mapper generic error", func(t *testing.T) {
d.mapper = &mockMapper{err: errors.New("boom")}
md := metadata.Pairs("machines", "any")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.Internal, st.Code())
})
}
func TestMapErrorToStatus(t *testing.T) {
t.Run("machines not found", func(t *testing.T) {
err := mapErrorToStatus(&MachinesNotFoundError{NotFound: []string{"a", "b"}})
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
})
t.Run("already grpc status", func(t *testing.T) {
original := status.Error(codes.DeadlineExceeded, "timeout")
err := mapErrorToStatus(original)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.DeadlineExceeded, st.Code())
})
t.Run("generic error", func(t *testing.T) {
err := mapErrorToStatus(errors.New("something broke"))
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.Internal, st.Code())
assert.Contains(t, st.Message(), "something broke")
})
}
+10 -14
View File
@@ -10,8 +10,9 @@ import (
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
) )
// LocalBackend is a proxy.Backend implementation that proxies to a local gRPC server listening on a Unix socket. // LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
type LocalBackend struct { type LocalBackend struct {
One2ManyResponder
sockPath string sockPath string
mu sync.RWMutex mu sync.RWMutex
@@ -20,15 +21,20 @@ type LocalBackend struct {
var _ proxy.Backend = (*LocalBackend)(nil) var _ proxy.Backend = (*LocalBackend)(nil)
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. // NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
func NewLocalBackend(sockPath string) *LocalBackend { // of the current machine which could be empty if it's not known. The address is used to populate response metadata
// in one2many mode.
func NewLocalBackend(sockPath, addr string) *LocalBackend {
return &LocalBackend{ return &LocalBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
sockPath: sockPath, sockPath: sockPath,
} }
} }
func (b *LocalBackend) String() string { func (b *LocalBackend) String() string {
return "unix://" + b.sockPath return b.machine
} }
// GetConnection returns a gRPC connection to the local server listening on the Unix socket. // GetConnection returns a gRPC connection to the local server listening on the Unix socket.
@@ -58,16 +64,6 @@ func (b *LocalBackend) GetConnection(ctx context.Context, _ string) (context.Con
return outCtx, b.conn, err return outCtx, b.conn, err
} }
// AppendInfo is a no-op for LocalBackend as it does not inject metadata.
func (b *LocalBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
return resp, nil
}
// BuildError is a no-op for LocalBackend.
func (b *LocalBackend) BuildError(streaming bool, err error) ([]byte, error) {
return nil, err
}
// Close closes the upstream gRPC connection. // Close closes the upstream gRPC connection.
func (b *LocalBackend) Close() { func (b *LocalBackend) Close() {
b.mu.Lock() b.mu.Lock()
-108
View File
@@ -1,108 +0,0 @@
package proxy
import (
"context"
"fmt"
"slices"
"strings"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
// MachineTarget represents a resolved machine target.
type MachineTarget struct {
ID, Name, Addr string
}
// MachinesNotFoundError indicates that one or more requested machines were not found.
type MachinesNotFoundError struct {
NotFound []string
}
func (e *MachinesNotFoundError) Error() string {
if len(e.NotFound) == 1 {
return fmt.Sprintf("machine not found: %s", e.NotFound[0])
}
return fmt.Sprintf("machines not found: %s", strings.Join(e.NotFound, ", "))
}
// MachineMapper provides access to machine information in the cluster.
type MachineMapper interface {
// MapMachines resolves a list of machine names/IDs (or "*") to a list of machine targets.
// Returns MachinesNotFoundError if any requested machine is not found (except when "*" is used).
MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error)
}
// Store is the interface required by MachineMapper to access the cluster store.
type Store interface {
ListMachines(ctx context.Context) ([]*pb.MachineInfo, error)
}
// CorrosionMapper implements MachineMapper using the corrosion store.
type CorrosionMapper struct {
store Store
}
func NewCorrosionMapper(store Store) *CorrosionMapper {
return &CorrosionMapper{store: store}
}
func (m *CorrosionMapper) MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error) {
if len(namesOrIDs) == 0 {
return nil, fmt.Errorf("no machines specified")
}
machines, err := m.store.ListMachines(ctx)
if err != nil {
return nil, fmt.Errorf("list machines: %w", err)
}
allTargets := make([]MachineTarget, 0, len(machines))
for _, machine := range machines {
ip, err := machine.Network.ManagementIp.ToAddr()
if err != nil {
return nil, fmt.Errorf("invalid management IP for machine '%s' in store: %w", machine.Name, err)
}
allTargets = append(allTargets, MachineTarget{
ID: machine.Id,
Name: machine.Name,
Addr: ip.String(),
})
}
if slices.Contains(namesOrIDs, "*") {
if len(allTargets) == 0 {
return nil, fmt.Errorf("no machines in cluster")
}
return allTargets, nil
}
// Build a map for lookup (keyed by both ID and name)
targetByLookup := make(map[string]MachineTarget, len(allTargets)*2)
for _, t := range allTargets {
targetByLookup[t.ID] = t
targetByLookup[t.Name] = t
}
// Resolve each requested machine.
targets := make([]MachineTarget, 0, len(namesOrIDs))
var notFound []string
seenTarget := make(map[string]struct{}, len(namesOrIDs))
for _, nameOrID := range namesOrIDs {
if t, ok := targetByLookup[nameOrID]; ok {
if _, seen := seenTarget[t.ID]; !seen {
targets = append(targets, t)
seenTarget[t.ID] = struct{}{}
}
} else {
notFound = append(notFound, nameOrID)
}
}
if len(notFound) > 0 {
return nil, &MachinesNotFoundError{NotFound: notFound}
}
return targets, nil
}
-168
View File
@@ -1,168 +0,0 @@
package proxy
import (
"context"
"errors"
"net/netip"
"testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockStore struct {
machines []*pb.MachineInfo
err error
}
func (s *mockStore) ListMachines(_ context.Context) ([]*pb.MachineInfo, error) {
if s.err != nil {
return nil, s.err
}
return s.machines, nil
}
func machineInfo(id, name, ip string) *pb.MachineInfo {
return &pb.MachineInfo{
Id: id,
Name: name,
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr(ip)),
},
}
}
func TestCorrosionMapper_MapMachines(t *testing.T) {
ctx := context.Background()
machines := []*pb.MachineInfo{
machineInfo("id-1", "machine-a", "fd00::1"),
machineInfo("id-2", "machine-b", "fd00::2"),
}
tests := []struct {
name string
store *mockStore
input []string
want []MachineTarget
wantErr bool
errMsg string
}{
{
name: "wildcard returns all machines",
store: &mockStore{machines: machines},
input: []string{"*"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "single name",
store: &mockStore{machines: machines},
input: []string{"machine-a"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "single id",
store: &mockStore{machines: machines},
input: []string{"id-2"},
want: []MachineTarget{
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "multiple mixed",
store: &mockStore{machines: machines},
input: []string{"machine-a", "id-2"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "deduplicates repeated inputs",
store: &mockStore{machines: machines},
input: []string{"machine-a", "machine-a"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "deduplicates name and id for same machine",
store: &mockStore{machines: machines},
input: []string{"machine-a", "id-1"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "not found single",
store: &mockStore{machines: machines},
input: []string{"missing"},
wantErr: true,
errMsg: "machine not found: missing",
},
{
name: "not found multiple",
store: &mockStore{machines: machines},
input: []string{"missing", "also-missing"},
wantErr: true,
errMsg: "machines not found: missing, also-missing",
},
{
name: "partial not found",
store: &mockStore{machines: machines},
input: []string{"machine-a", "missing"},
wantErr: true,
errMsg: "machine not found: missing",
},
{
name: "wildcard with no machines",
store: &mockStore{machines: []*pb.MachineInfo{}},
input: []string{"*"},
wantErr: true,
errMsg: "no machines in cluster",
},
{
name: "store error",
store: &mockStore{err: errors.New("store down")},
input: []string{"*"},
wantErr: true,
errMsg: "list machines: store down",
},
{
name: "invalid management ip",
store: &mockStore{machines: []*pb.MachineInfo{{Id: "bad", Name: "bad-ip", Network: &pb.NetworkConfig{ManagementIp: &pb.IP{}}}}},
input: []string{"*"},
wantErr: true,
errMsg: "invalid management IP for machine 'bad-ip' in store",
},
{
name: "empty input returns error",
store: &mockStore{machines: machines},
input: []string{},
wantErr: true,
errMsg: "no machines specified",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mapper := NewCorrosionMapper(tt.store)
got, err := mapper.MapMachines(ctx, tt.input)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
+7 -13
View File
@@ -14,11 +14,13 @@ import (
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
) )
// RemoteBackend is a proxy.Backend implementation that proxies to a remote gRPC server. // RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
// into the response.
// //
// Based on the Talos apid implementation: // Based on the Talos apid implementation:
// https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go // https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go
type RemoteBackend struct { type RemoteBackend struct {
One2ManyResponder
target string target string
mu sync.RWMutex mu sync.RWMutex
@@ -35,12 +37,15 @@ func NewRemoteBackend(addr string, port uint16) (*RemoteBackend, error) {
} }
return &RemoteBackend{ return &RemoteBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
target: netip.AddrPortFrom(ip, port).String(), target: netip.AddrPortFrom(ip, port).String(),
}, nil }, nil
} }
func (b *RemoteBackend) String() string { func (b *RemoteBackend) String() string {
return b.target return b.machine
} }
// GetConnection returns a gRPC connection to the remote server. // GetConnection returns a gRPC connection to the remote server.
@@ -53,7 +58,6 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
} }
delete(md, ":authority") delete(md, ":authority")
delete(md, "machines") delete(md, "machines")
delete(md, "machine")
outCtx := metadata.NewOutgoingContext(ctx, md) outCtx := metadata.NewOutgoingContext(ctx, md)
@@ -96,16 +100,6 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
return outCtx, b.conn, err return outCtx, b.conn, err
} }
// AppendInfo is a no-op for RemoteBackend as it does not inject metadata.
func (b *RemoteBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
return resp, nil
}
// BuildError is a no-op for RemoteBackend.
func (b *RemoteBackend) BuildError(streaming bool, err error) ([]byte, error) {
return nil, err
}
// Close closes the upstream gRPC connection. // Close closes the upstream gRPC connection.
func (b *RemoteBackend) Close() { func (b *RemoteBackend) Close() {
b.mu.Lock() b.mu.Lock()
+1 -1
View File
@@ -107,7 +107,7 @@ func (c *Controller) Run(ctx context.Context) error {
select { select {
case _, ok := <-changes: case _, ok := <-changes:
if !ok { if !ok {
return fmt.Errorf("subscription to container changes in cluster store failed") return fmt.Errorf("containers subscription failed")
} }
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.") c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
+161 -298
View File
@@ -15,48 +15,32 @@ import (
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/caddyconfig" "github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/constants" "github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/corromigrate"
"github.com/psviderski/uncloud/internal/machine/corroservice" "github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns" "github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/firewall" "github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/metrics"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/unregistry" "github.com/psviderski/unregistry"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/protobuf/proto"
) )
// machineSyncInterval is how often the machine info is republished to the cluster store to recover from
// failed synchronous syncs.
const machineSyncInterval = 60 * time.Second
// clusterController is the main controller for the machine that is a cluster member. It manages components such as // clusterController is the main controller for the machine that is a cluster member. It manages components such as
// the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers, // the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers,
// and others. // and others.
type clusterController struct { type clusterController struct {
// machine is the parent machine. state *State
machine *Machine store *store.Store
state *State
store *store.Store
wgnet *network.WireGuardNetwork wgnet *network.WireGuardNetwork
endpointChanges <-chan network.EndpointChangeEvent endpointChanges <-chan network.EndpointChangeEvent
server *grpc.Server server *grpc.Server
corroService corroservice.Service corroService corroservice.Service
// corrosionDir is the disk path that holds the Corrosion config and data. dockerCtrl *docker.Controller
// TODO: remove in 0.22 assuming all pre 0.20 clusters upgraded their pre-v1 Corrosion.
corrosionDir string
dockerService *docker.Service
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{}
// syncMachineTrigger requests to sync the machine info to the cluster store.
syncMachineTrigger chan struct{}
// clusterReady is signalled when the cluster controller has finished initializing all components. // clusterReady is signalled when the cluster controller has finished initializing all components.
clusterReady chan<- struct{} clusterReady chan<- struct{}
caddyconfigCtrl *caddyconfig.Controller caddyconfigCtrl *caddyconfig.Controller
@@ -67,18 +51,15 @@ type clusterController struct {
// unregistry is the embedded container registry that uses the local Docker (containerd) image store as its backend. // unregistry is the embedded container registry that uses the local Docker (containerd) image store as its backend.
unregistry *unregistry.Registry unregistry *unregistry.Registry
metricsServer *metrics.Server
// stopped is a channel that is closed when the controller is stopped. // stopped is a channel that is closed when the controller is stopped.
stopped chan struct{} stopped chan struct{}
} }
func newClusterController( func newClusterController(
machine *Machine, state *State,
store *store.Store, store *store.Store,
server *grpc.Server, server *grpc.Server,
corroService corroservice.Service, corroService corroservice.Service,
corrosionDir string,
dockerService *docker.Service, dockerService *docker.Service,
dockerReady chan<- struct{}, dockerReady chan<- struct{},
clusterReady chan<- struct{}, clusterReady chan<- struct{},
@@ -86,7 +67,6 @@ func newClusterController(
dnsServer *dns.Server, dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver, dnsResolver *dns.ClusterResolver,
unregistry *unregistry.Registry, unregistry *unregistry.Registry,
metricsServer *metrics.Server,
) (*clusterController, error) { ) (*clusterController, error) {
slog.Info("Starting WireGuard network.") slog.Info("Starting WireGuard network.")
wgnet, err := network.NewWireGuardNetwork() wgnet, err := network.NewWireGuardNetwork()
@@ -96,33 +76,27 @@ func newClusterController(
endpointChanges := wgnet.WatchEndpoints() endpointChanges := wgnet.WatchEndpoints()
return &clusterController{ return &clusterController{
machine: machine, state: state,
state: machine.state, store: store,
store: store, wgnet: wgnet,
wgnet: wgnet, endpointChanges: endpointChanges,
endpointChanges: endpointChanges, server: server,
server: server, corroService: corroService,
corroService: corroService, dockerCtrl: docker.NewController(state.ID, dockerService, store),
corrosionDir: corrosionDir, dockerReady: dockerReady,
dockerService: dockerService, clusterReady: clusterReady,
dockerCtrl: docker.NewController(machine.state.ID, dockerService, store), caddyconfigCtrl: caddyfileCtrl,
dockerReady: dockerReady, dnsServer: dnsServer,
syncMachineTrigger: make(chan struct{}, 1), dnsResolver: dnsResolver,
clusterReady: clusterReady, unregistry: unregistry,
caddyconfigCtrl: caddyfileCtrl, stopped: make(chan struct{}),
dnsServer: dnsServer,
dnsResolver: dnsResolver,
unregistry: unregistry,
metricsServer: metricsServer,
stopped: make(chan struct{}),
}, nil }, nil
} }
func (cc *clusterController) Run(ctx context.Context) error { func (cc *clusterController) Run(ctx context.Context) error {
defer close(cc.stopped) defer close(cc.stopped)
if err := firewall.ConfigureIptablesChains(network.MachineIP(cc.state.Network.Subnet), if err := firewall.ConfigureIptablesChains(network.MachineIP(cc.state.Network.Subnet)); err != nil {
cc.state.Network.EffectiveWireGuardPort()); err != nil {
return fmt.Errorf("configure iptables chains: %w", err) return fmt.Errorf("configure iptables chains: %w", err)
} }
@@ -151,11 +125,6 @@ func (cc *clusterController) Run(ctx context.Context) error {
slog.Info("Corrosion service started.") slog.Info("Corrosion service started.")
} }
// Apply the seed to finish Corrosion migrations from 0.x to 2026.x.x (upstream v1.0.0) if applicable.
if err := corromigrate.ApplySeedIfPresent(ctx, cc.corrosionDir, cc.store); err != nil {
return fmt.Errorf("apply corrosion migration seed: %w", err)
}
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 // Start the WireGuard control loop before waiting for store sync. This ensures endpoint rotation happens
@@ -173,9 +142,7 @@ func (cc *clusterController) Run(ctx context.Context) error {
return nil return nil
}) })
// Start the network API server before waiting for the store sync so the machine is reachable on the mesh // Start the network API server. Assume the management IP can't be changed when the network is running.
// during the sync and can serve requests that don't depend on the store.
// 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)
if err != nil { if err != nil {
@@ -192,23 +159,18 @@ func (cc *clusterController) Run(ctx context.Context) error {
// Wait for the store database to sync to the minimum version before starting store-dependent components. // 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, // 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. // e.g., an empty machine list causing WireGuard peer misconfiguration.
if err = cc.waitStoreSync(ctx); err != nil { cc.waitStoreSync(ctx)
return fmt.Errorf("wait initial cluster store sync: %w", err)
}
// Check if waitStoreSync exited because the context was cancelled. Return early in that case. // Check if waitStoreSync exited because the context was cancelled. Return early in that case.
if ctx.Err() != nil { if ctx.Err() != nil {
cc.stopAPIServer() cc.stopAPIServer()
return errGroup.Wait()
}
errGroup.Go(func() error { err := errGroup.Wait()
slog.Info("Starting metrics server.") if corroErr := cc.stopCorrosion(); corroErr != nil {
if err := cc.metricsServer.Run(ctx); err != nil { err = errors.Join(err, corroErr)
return fmt.Errorf("metrics server failed: %w", err)
} }
return nil return err
}) }
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Starting embedded DNS resolver.") slog.Info("Starting embedded DNS resolver.")
@@ -227,11 +189,6 @@ func (cc *clusterController) Run(ctx context.Context) error {
return nil return nil
}) })
// Keep the machine info in the cluster store in sync with the actual machine state (the source of truth).
errGroup.Go(func() error {
return cc.runMachineSync(ctx)
})
// Synchronise Docker containers to the cluster store. // Synchronise Docker containers to the cluster store.
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Watching Docker containers and syncing them to cluster store.") slog.Info("Watching Docker containers and syncing them to cluster store.")
@@ -286,9 +243,15 @@ func (cc *clusterController) Run(ctx context.Context) error {
slog.Info("Unregistry server stopped.") slog.Info("Unregistry server stopped.")
} }
// Wait for all controllers to finish. The Corrosion service shutdown is handled by the machine after stopping all // Wait for all controllers to finish.
// local API servers that may still serve requests depending on the store. err = errGroup.Wait()
return errGroup.Wait()
// Stop Corrosion after all controllers depending on it and API server are stopped.
if corroErr := cc.stopCorrosion(); corroErr != nil {
err = errors.Join(err, corroErr)
}
return err
} }
// stopAPIServer gracefully stops the network API server with a timeout. // stopAPIServer gracefully stops the network API server with a timeout.
@@ -314,6 +277,19 @@ func (cc *clusterController) stopAPIServer() {
slog.Info("Network API server 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 {
@@ -323,7 +299,6 @@ func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
if err := cc.dockerCtrl.EnsureUncloudNetwork( if err := cc.dockerCtrl.EnsureUncloudNetwork(
ctx, ctx,
cc.state.Network.Subnet, cc.state.Network.Subnet,
cc.state.Network.EffectiveMTU(),
cc.dnsServer.ListenAddr(), cc.dnsServer.ListenAddr(),
); err != nil { ); err != nil {
return fmt.Errorf("ensure Docker network: %w", err) return fmt.Errorf("ensure Docker network: %w", err)
@@ -366,114 +341,101 @@ func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
} }
} }
// waitStoreSync blocks until the local store version >= state.MinStoreVersion and any known gaps are synced. // waitStoreSync waits for the store database to sync to the minimum required DB version if set in the machine state.
// No-op when MinStoreVersion is empty. Clears state.MinStoreVersion when reached. // Blocks until synced or context is cancelled.
func (cc *clusterController) waitStoreSync(ctx context.Context) error { func (cc *clusterController) waitStoreSync(ctx context.Context) {
target := cc.state.MinStoreVersion minVersion := cc.state.MinStoreDBVersion
if len(target) == 0 { if minVersion == 0 {
return nil return
} }
slog.Info("Waiting for the initial cluster store sync.", "actors", len(target)) slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
ticker := time.NewTicker(500 * time.Millisecond) ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
// Periodic warning to surface stuck NAT/connectivity issues without aborting.
warnInterval := 5 * time.Minute
warnTimer := time.NewTimer(warnInterval)
defer warnTimer.Stop()
var ( var (
lastLagging int lastVersion int64
lastLogTime time.Time
lastErrLogTime time.Time lastErrLogTime time.Time
) )
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return
case <-warnTimer.C:
local, err := cc.store.Version(ctx)
if err == nil {
slog.Error("Cluster store sync still pending. Check connectivity to peers.",
"lagging_actors", laggingActors(local, target))
} else {
slog.Error("Cluster store sync still pending. Check connectivity to peers.", "err", err)
}
warnTimer.Reset(warnInterval)
case <-ticker.C: case <-ticker.C:
local, err := cc.store.Version(ctx) version, err := cc.store.DBVersion(ctx)
if err != nil { if err != nil {
// Throttle error logs to once every 5 seconds. // Log errors at most once every 5 seconds.
if time.Since(lastErrLogTime) >= 5*time.Second { if time.Since(lastErrLogTime) >= 5*time.Second {
slog.Error("Failed to get the cluster store version, retrying.", "err", err) slog.Error("Failed to get the cluster store DB version, retrying.", "err", err)
lastErrLogTime = time.Now() lastErrLogTime = time.Now()
} }
continue continue
} }
lagging := laggingActors(local, target) if version >= minVersion {
if len(lagging) == 0 { // Clear MinStoreDBVersion so next restart doesn't wait for sync.
// Per-actor max doesn't imply contiguous apply: corrosion can buffer X:N before
// X:N-1 arrives and track the gap separately. Wait for any remaining gaps to be synced.
if err := cc.waitKnownMissingChanges(ctx); err != nil {
return fmt.Errorf("wait for known missing changes: %w", err)
}
// If the context was cancelled mid-gap-fill, don't persist a "synced" state.
if ctx.Err() != nil {
return nil
}
// Clear MinStoreVersion so next restart doesn't wait for sync.
cc.state.mu.Lock() cc.state.mu.Lock()
cc.state.MinStoreVersion = nil cc.state.MinStoreDBVersion = 0
err = cc.state.Save() if err := cc.state.Save(); err != nil {
cc.state.mu.Unlock() slog.Error("Failed to save machine state after the initial cluster store sync.", "err", err)
if err != nil {
return fmt.Errorf("save machine state after the initial cluster store sync: %w", err)
} }
cc.state.mu.Unlock()
slog.Info("Cluster store completed the initial sync.", "actors", len(target)) // Wait for all known missing changes to be synced before returning.
return nil // TODO: reevaluate if this is necessary after migrating to the latest Corrosion version:
// https://github.com/psviderski/uncloud/issues/172
// This works on the best effort basis as the missing changes may not be yet known when we start
// checking it after reaching the minimum version.
// Reaching the minimum version doesn't guarantee that the store is actually synced to
// the state we observed on the source node. db_version is a machine-local Lamport clock.
// When changes are received from a remote machine, the local db_version is set to
// max(local_db_version, incoming_db_version) + 1 for each applied transaction. This means
// the new machine's db_version can jump well past the source machine's db_version on the very
// first batch of replicated changes, without having received all changes from all machines
// in the cluster.
cc.waitKnownMissingChanges(ctx)
if ver, verErr := cc.store.DBVersion(ctx); verErr == nil {
version = ver
}
slog.Info("Cluster store completed the initial sync.", "version", version, "min_version", minVersion)
return
} }
if len(lagging) != lastLagging { // Log progress only once a second.
slog.Info("Syncing cluster store.", "lagging_actors", lagging) if version != lastVersion && time.Since(lastLogTime) >= 1*time.Second {
lastLagging = len(lagging) slog.Info("Syncing cluster store.", "version", version, "min_version", minVersion)
lastLogTime = time.Now()
lastVersion = version
} }
} }
} }
} }
// laggingActors returns target actors whose local version is below the required value, as [have, need].
func laggingActors(local, target map[string]int64) map[string][2]int64 {
lagging := make(map[string][2]int64)
for actor, need := range target {
if have := local[actor]; have < need {
lagging[actor] = [2]int64{have, need}
}
}
return lagging
}
// waitKnownMissingChanges polls the store until all known missing changes have been synced. // waitKnownMissingChanges polls the store until all known missing changes have been synced.
func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) error { func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return
case <-ticker.C: case <-ticker.C:
changes, err := cc.store.KnownMissingChanges(ctx) changes, err := cc.store.KnownMissingChanges(ctx)
if err != nil { if err != nil {
return fmt.Errorf("query known missing changes from cluster store: %w", err) slog.Error("Failed to get known missing changes from the cluster store, skipping check.",
"err", err)
return
} }
if len(changes) == 0 { if len(changes) == 0 {
slog.Debug("All known missing changes have been synced to the cluster store.") slog.Debug("All known missing changes have been synced to the cluster store.")
return nil return
} }
slog.Debug("Waiting for known missing changes to be synced to the cluster store.", "remaining", slog.Debug("Waiting for known missing changes to be synced to the cluster store.", "remaining",
@@ -482,131 +444,10 @@ func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) error
} }
} }
// runMachineSync keeps this machine's info in the cluster store in sync with the local state and the Docker engine
// version. Call RequestMachineSync to trigger an immediate sync.
func (cc *clusterController) runMachineSync(ctx context.Context) error {
// Backfill legacy local state from the cluster store once before the first sync.
if err := cc.backfillMachineState(ctx); err != nil {
return fmt.Errorf("backfill machine state: %w", err)
}
if err := cc.syncMachineInfo(ctx); err != nil {
slog.Error("Failed to sync machine info to cluster store.", "err", err)
}
ticker := time.NewTicker(machineSyncInterval)
defer ticker.Stop()
dockerRestarted := cc.dockerService.WatchDaemonRestart(ctx)
for {
select {
case <-ctx.Done():
case <-ticker.C: // Scheduled periodic sync.
case <-cc.syncMachineTrigger: // Immediate sync request.
case <-dockerRestarted: // Docker daemon restarted -- engine version may have changed.
}
// A pending restart signal can race with context cancellation and win the select.
if ctx.Err() != nil {
return nil
}
if err := cc.syncMachineInfo(ctx); err != nil {
slog.Error("Failed to sync machine info to cluster store.", "err", err)
}
}
}
// RequestMachineSync triggers an immediate sync of this machine's info to the cluster store.
// It never blocks and coalesces with any already pending sync.
func (cc *clusterController) RequestMachineSync() {
select {
case cc.syncMachineTrigger <- struct{}{}:
default:
}
}
// syncMachineInfo republishes this machine's info from local state to the cluster store, skipping the
// write if the info is unchanged since the last successful write.
func (cc *clusterController) syncMachineInfo(ctx context.Context) error {
publishedInfo, err := cc.store.GetMachine(ctx, cc.state.ID)
if err != nil && !errors.Is(err, store.ErrMachineNotFound) {
return fmt.Errorf("get machine from store: %w", err)
}
info := cc.machine.Info(ctx)
if publishedInfo != nil {
// Info leaves the Docker engine version empty when the engine is unavailable. Keep the previously
// published version in that case rather than overwriting it with an empty value.
if info.DockerVersion == "" {
info.DockerVersion = publishedInfo.DockerVersion
}
// Skip the write if nothing changed since the last successful sync.
if proto.Equal(info, publishedInfo) {
return nil
}
}
if err = cc.store.UpdateMachine(ctx, info); err != nil {
if !errors.Is(err, store.ErrMachineNotFound) {
return fmt.Errorf("update machine in store: %w", err)
}
// The machine row is missing (not created yet or lost). Recreate it.
if err = cc.store.CreateMachine(ctx, info); err != nil {
return fmt.Errorf("create machine in store: %w", err)
}
}
slog.Info("Synced machine info to cluster store.", "id", info.Id, "name", info.Name)
return nil
}
// backfillMachineState backfills legacy local state that predates local ownership of endpoints/public IP
// (implemented in 0.20) from the cluster store.
// TODO: remove after releasing 0.22.
func (cc *clusterController) backfillMachineState(ctx context.Context) error {
cc.state.mu.Lock()
defer cc.state.mu.Unlock()
if len(cc.state.Network.Endpoints) > 0 && cc.state.PublicIP.IsValid() {
return nil
}
existing, err := cc.store.GetMachine(ctx, cc.state.ID)
if err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
// No existing row to backfill from. syncMachineInfo will create it.
return nil
}
return fmt.Errorf("get machine from store: %w", err)
}
changed := false
if len(cc.state.Network.Endpoints) == 0 {
if endpoints := endpointsToAddrPorts(existing.Network.GetEndpoints()); len(endpoints) > 0 {
cc.state.Network.Endpoints = endpoints
changed = true
}
}
if !cc.state.PublicIP.IsValid() && existing.PublicIp != nil {
if ip, _ := existing.PublicIp.ToAddr(); ip.IsValid() {
cc.state.PublicIP = ip
changed = true
}
}
if changed {
if err = cc.state.Save(); err != nil {
return fmt.Errorf("save backfilled machine state: %w", err)
}
}
return nil
}
// 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 {
// Supervise the watch-and-sync pipeline until the context is done. // Retry to watch and sync containers until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff( boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond), backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(5*time.Second), backoff.WithMaxInterval(5*time.Second),
@@ -620,7 +461,7 @@ func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
return nil return nil
} }
if err := backoff.Retry(watchAndSync, boff); err != nil { if err := backoff.Retry(watchAndSync, boff); err != nil {
if ctx.Err() != nil { if errors.Is(err, context.Canceled) {
return nil return nil
} }
return fmt.Errorf("watch and sync containers to cluster store: %w", err) return fmt.Errorf("watch and sync containers to cluster store: %w", err)
@@ -630,52 +471,74 @@ func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
} }
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly // handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly
// when changes occur. It returns an error when the subscription fails. // when changes occur.
func (cc *clusterController) handleMachineChanges(ctx context.Context) error { func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
machines, changes, err := cc.store.SubscribeMachines(ctx) for {
if err != nil { // Retry to subscribe to machine changes indefinitely until the context is done.
return fmt.Errorf("subscribe to machine changes: %w", err) boff := backoff.WithContext(backoff.NewExponentialBackOff(
} backoff.WithInitialInterval(1*time.Second),
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.") backoff.WithMaxInterval(60*time.Second),
backoff.WithMaxElapsedTime(0),
), ctx)
// Assume the initial store synchronization when this machine first joined the cluster has already been completed. var (
// So the machines should not be empty. But we still have a safety check to not reconfigure with an empty list, machines []*pb.MachineInfo
// which would remove all peers and lock this machine out of the cluster. changes <-chan struct{}
// A list containing only this machine is a valid state (e.g. all other machines were removed) and should still err error
// trigger reconfiguration to drop any stale peers. )
if len(machines) > 0 { subscribe := func() error {
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines)) if machines, changes, err = cc.store.SubscribeMachines(ctx); err != nil {
if err = cc.configurePeers(machines); err != nil { slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
slog.Error("Failed to configure peers.", "err", err) }
return err
} }
} if err = backoff.Retry(subscribe, boff); err != nil {
if errors.Is(err, context.Canceled) {
// For simplicity, reconfigure all peers on any change. The subscription closes the changes channel both on context return nil
// cancellation and when the subscription fails, so this loop exits in both cases. }
for range changes { slog.Error("Unexpected error while retrying to subscribe to machine changes.", "err", err)
slog.Info("Cluster machines changed, reconfiguring network peers.")
if machines, err = cc.store.ListMachines(ctx); err != nil {
slog.Error("Failed to list machines.", "err", err)
continue continue
} }
// A safety check for the exceptional case when something bad happened with the store. Reconfiguring with an slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
// empty list would remove all peers and lock this machine out of the cluster.
// See https://github.com/psviderski/uncloud/issues/155.
if len(machines) == 0 {
slog.Debug("Skipping peer reconfiguration: machines list in store is empty.")
continue
}
if err = cc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
}
// The changes channel was closed. It's a clean shutdown if the context was cancelled, otherwise the subscription // The machine store may be empty when a machine first joins the cluster, before store synchronization
// failed and we return an error to fail the controller. // completes. Skip configuration now and apply it when the store changes are received.
if ctx.Err() != nil { // TODO: remove this check after ensuring the store is actually synced to the latest known state at this point.
return nil // See TODO in waitStoreSync.
if len(machines) > 0 {
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
if err = cc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
}
// For simplicity, reconfigure all peers on any change.
for {
select {
// TODO: test when Corrosion fails and the subscription fails to resubscribe (after 1 minute). It seems
// the changes channel will be closed and this will become a busy loop. Perhaps, the outer for loop should
// be reworked as well.
case <-changes:
slog.Info("Cluster machines changed, reconfiguring network peers.")
if machines, err = cc.store.ListMachines(ctx); err != nil {
slog.Error("Failed to list machines.", "err", err)
continue
}
// Skip reconfiguration if the machines list is empty. This can happen when joining the cluster.
// Corrosion can notifies about table changes before the data is fully replicated.
// Reconfiguring with an empty list would remove all peers and lock this machine out of the cluster.
// See https://github.com/psviderski/uncloud/issues/155.
if len(machines) == 0 {
slog.Debug("Skipping peer reconfiguration: machines list in store is empty.")
continue
}
if err = cc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
case <-ctx.Done():
return nil
}
}
} }
return fmt.Errorf("subscription to machine changes in cluster store failed")
} }
func (cc *clusterController) configurePeers(machines []*pb.MachineInfo) error { func (cc *clusterController) configurePeers(machines []*pb.MachineInfo) error {
+83 -7
View File
@@ -195,6 +195,89 @@ func (c *Cluster) network(ctx context.Context) (netip.Prefix, error) {
return prefix, nil return prefix, nil
} }
// UpdateMachine updates machine configuration in the cluster.
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
if err := c.checkReady(); err != nil {
return nil, err
}
if req.MachineId == "" {
return nil, status.Error(codes.InvalidArgument, "machine_id not set")
}
// Get the current machine info
currentMachine, err := c.store.GetMachine(ctx, req.MachineId)
if err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
}
return nil, status.Errorf(codes.Internal, "failed to get machine: %v", err)
}
// Create a copy of the current machine for updating
updatedMachine := &pb.MachineInfo{
Id: currentMachine.Id,
Name: currentMachine.Name,
Network: currentMachine.Network,
PublicIp: currentMachine.PublicIp,
}
// Apply updates from the request
if req.Name != nil {
// Check for empty name
if *req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "machine name cannot be empty")
}
// Check for duplicate names (excluding the current machine)
if *req.Name != currentMachine.Name {
machines, err := c.store.ListMachines(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
}
for _, m := range machines {
if m.Id != req.MachineId && m.Name == *req.Name {
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", *req.Name)
}
}
}
updatedMachine.Name = *req.Name
}
if req.PublicIp != nil {
// Check if this is an empty IP (used to signal removal)
if len(req.PublicIp.Ip) == 0 {
// User wants to remove public IP
updatedMachine.PublicIp = nil
} else {
// Validate and set the new IP
ip, err := req.PublicIp.ToAddr()
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
}
if !ip.IsValid() {
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
}
updatedMachine.PublicIp = req.PublicIp
}
}
if len(req.Endpoints) > 0 {
updatedMachine.Network.Endpoints = req.Endpoints
}
// Update the machine in the store
if err = c.store.UpdateMachine(ctx, updatedMachine); err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
}
return nil, status.Errorf(codes.Internal, "update machine: %v", err)
}
slog.Info("Machine configuration updated in the cluster.",
"id", updatedMachine.Id, "name", updatedMachine.Name)
resp := &pb.UpdateMachineResponse{Machine: updatedMachine}
return resp, nil
}
// 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.checkReady(); err != nil { if err := c.checkReady(); err != nil {
@@ -251,13 +334,6 @@ func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineReques
return nil, status.Error(codes.InvalidArgument, "machine ID not set") return nil, status.Error(codes.InvalidArgument, "machine ID not set")
} }
// Cleanup machine containers from the store that could be left if the machine is unavailable
// removed with --no-reset, or didn't have time to finish propagating changes before resetting.
if err := c.store.DeleteContainers(ctx, store.DeleteOptions{MachineIDs: []string{req.Id}}); err != nil {
slog.Error("Failed to delete container records from the cluster store for the machine being removed.",
"id", req.Id, "err", err)
}
if err := c.store.DeleteMachine(ctx, req.Id); err != nil { if err := c.store.DeleteMachine(ctx, req.Id); err != nil {
if errors.Is(err, store.ErrMachineNotFound) { if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.Id) return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.Id)

Some files were not shown because too many files have changed in this diff Show More