mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
57
Commits
hub-landing
...
v0.20.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5d73fe4d7 | ||
|
|
e01c8d3cc7 | ||
|
|
925695a44a | ||
|
|
e5b205391e | ||
|
|
e9762dda21 | ||
|
|
5bfdbd5c8f | ||
|
|
1d5c71730f | ||
|
|
ad43569fe5 | ||
|
|
9ea30e6ee9 | ||
|
|
a040ce5f8e | ||
|
|
328d17599b | ||
|
|
83256b5bf7 | ||
|
|
35191c00e0 | ||
|
|
b08cee03e3 | ||
|
|
99e1ae0edb | ||
|
|
5f07bc5832 | ||
|
|
8aa0a96c23 | ||
|
|
8b95db66d1 | ||
|
|
69acf6f58e | ||
|
|
18b4112a33 | ||
|
|
23d6b4409a | ||
|
|
0c42a7df65 | ||
|
|
e3823c29bf | ||
|
|
2f717a10a7 | ||
|
|
7bb3cb9682 | ||
|
|
247154eeb3 | ||
|
|
7bac40ae3e | ||
|
|
d36ff67409 | ||
|
|
3841da9e1d | ||
|
|
cffa03007b | ||
|
|
73d515bb40 | ||
|
|
0c3b8b122f | ||
|
|
2f76187bf5 | ||
|
|
f275a31096 | ||
|
|
e622c06ca6 | ||
|
|
fffc6a8b8a | ||
|
|
cccb8635e1 | ||
|
|
1247f961c2 | ||
|
|
3586a32987 | ||
|
|
24b81af43c | ||
|
|
11df8e4a75 | ||
|
|
9424daff28 | ||
|
|
26951773d1 | ||
|
|
7616f1365a | ||
|
|
2aaaa70b15 | ||
|
|
442b5c62bf | ||
|
|
71da32549a | ||
|
|
9236aa341a | ||
|
|
0dd4bc9d36 | ||
|
|
07acb31bf3 | ||
|
|
a26af3b2ae | ||
|
|
29faeef5a5 | ||
|
|
2f159157cc | ||
|
|
28c88e4e10 | ||
|
|
7f7d091429 | ||
|
|
424263bdd4 | ||
|
|
c95136eae6 |
+19
-10
@@ -20,9 +20,9 @@ snapshot:
|
|||||||
version_template: "{{ incminor .Version }}-nightly-{{.ShortCommit}}"
|
version_template: "{{ incminor .Version }}-nightly-{{.ShortCommit}}"
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: uncloud
|
- id: uc
|
||||||
main: ./cmd/uncloud
|
main: ./cmd/uc
|
||||||
binary: uncloud
|
binary: uc
|
||||||
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,7 +34,12 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
- -s -w
|
||||||
|
-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
|
||||||
@@ -47,12 +52,17 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
- -s -w
|
||||||
|
-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: uncloud
|
- id: uc
|
||||||
builds:
|
builds:
|
||||||
- uncloud
|
- uc
|
||||||
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
|
||||||
@@ -93,10 +103,9 @@ brews:
|
|||||||
description: "Uncloud CLI"
|
description: "Uncloud CLI"
|
||||||
homepage: "https://uncloud.run"
|
homepage: "https://uncloud.run"
|
||||||
ids:
|
ids:
|
||||||
- uncloud
|
- uc
|
||||||
install: |
|
install: |
|
||||||
bin.install "uncloud"
|
bin.install "uc"
|
||||||
bin.install_symlink "uncloud" => "uc"
|
|
||||||
skip_upload: false
|
skip_upload: false
|
||||||
repository:
|
repository:
|
||||||
owner: psviderski
|
owner: psviderski
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ management.
|
|||||||
|
|
||||||
- **`cmd/`**: Contains main applications
|
- **`cmd/`**: Contains main applications
|
||||||
|
|
||||||
- `uncloud/`: CLI tool with subcommands for machine, service, volume management
|
- `uc/`: 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 uncloud ./cmd/uncloud
|
go build -o uc ./cmd/uc
|
||||||
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/uncloud/main.go`: CLI entry point and command structure
|
- `cmd/uc/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
|
||||||
|
|||||||
+9
-5
@@ -18,6 +18,7 @@ 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" ;; \
|
||||||
@@ -25,20 +26,21 @@ 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/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
|
"https://github.com/psviderski/corrosion/releases/download/v${CORROSION_VERSION}/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/local/bin/corrosion \
|
&& install /tmp/corrosion /usr/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-debian12:latest AS corrosion
|
FROM gcr.io/distroless/cc-debian13:latest AS corrosion
|
||||||
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
COPY --from=corrosion-download /usr/bin/corrosion /usr/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_IMAGE="ghcr.io/psviderski/corrosion:latest"
|
ARG CORROSION_VERSION
|
||||||
|
ARG CORROSION_IMAGE="ghcr.io/unlabs-dev/corrosion:${CORROSION_VERSION}"
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
@@ -47,6 +49,8 @@ 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
@@ -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/uncloud
|
go build -o uc ./cmd/uc
|
||||||
```
|
```
|
||||||
|
|
||||||
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/uncloud --help
|
go run ./cmd/uc --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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# 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_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
CORROSION_VERSION ?= 2026.6.15
|
||||||
|
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
|
||||||
@@ -9,11 +10,12 @@ ucind-cluster:
|
|||||||
|
|
||||||
.PHONY: corrosion-image
|
.PHONY: corrosion-image
|
||||||
corrosion-image:
|
corrosion-image:
|
||||||
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
docker build --build-arg CORROSION_VERSION=$(CORROSION_VERSION) -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 --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
|
docker buildx build --build-arg CORROSION_VERSION=$(CORROSION_VERSION) --push --platform linux/amd64,linux/arm64 \
|
||||||
|
-t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: ucind-multiarch-image-push
|
.PHONY: ucind-multiarch-image-push
|
||||||
ucind-multiarch-image-push:
|
ucind-multiarch-image-push:
|
||||||
@@ -70,4 +72,4 @@ _lint:
|
|||||||
|
|
||||||
.PHONY: cli-docs
|
.PHONY: cli-docs
|
||||||
cli-docs:
|
cli-docs:
|
||||||
go run ./cmd/uncloud docs
|
go run ./cmd/uc docs
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
_ _ ___
|
||||||
|
| | | |/ __|
|
||||||
|
| |_| | (__
|
||||||
|
\__,_|\___|
|
||||||
@@ -48,10 +48,7 @@ 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, _, err = clusterClient.ProxyMachinesContext(ctx, []string{opts.machine})
|
ctx = clusterClient.ProxySingleMachineContext(ctx, opts.machine)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
|
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewConfigCommand(),
|
NewConfigCommand(),
|
||||||
NewDeployCommand(),
|
NewDeployCommand(),
|
||||||
|
NewLogsCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ 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,6 +5,7 @@ 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"
|
||||||
@@ -15,6 +16,7 @@ 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)
|
||||||
@@ -49,6 +51,6 @@ func list(uncli *cli.CLI) error {
|
|||||||
t.Row(name, current, fmt.Sprintf("%d", connCount))
|
t.Row(name, current, fmt.Sprintf("%d", connCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ 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,6 +11,7 @@ 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,6 +8,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ 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)
|
||||||
|
|
||||||
@@ -49,6 +51,10 @@ 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))
|
||||||
|
|
||||||
@@ -163,6 +163,14 @@ 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,
|
||||||
@@ -203,7 +211,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.IsStdinTerminal() {
|
if !tui.IsTerminalAvailable() {
|
||||||
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/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uc/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"
|
||||||
@@ -88,19 +88,6 @@ 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{
|
||||||
@@ -115,12 +102,15 @@ 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 {
|
||||||
// Get machine name for better readability.
|
if err := machineImages.Error(); err != nil {
|
||||||
machineName := machineImages.Metadata.Machine
|
tui.PrintWarning(fmt.Sprintf("failed to list images on machine '%s': %s",
|
||||||
if m := allMachines.FindByNameOrID(machineName); m != nil {
|
machineImages.Metadata.MachineName, err))
|
||||||
machineName = m.Machine.Name
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get machine name for better readability.
|
||||||
|
machineName := machineImages.Metadata.MachineName
|
||||||
|
|
||||||
store := "docker"
|
store := "docker"
|
||||||
if machineImages.ContainerdStore {
|
if machineImages.ContainerdStore {
|
||||||
store = "containerd"
|
store = "containerd"
|
||||||
@@ -189,7 +179,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.
|
||||||
fmt.Println(formatImageTable(rows))
|
lipgloss.Println(formatImageTable(rows))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/image"
|
"github.com/psviderski/uncloud/cmd/uc/image"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,10 +8,9 @@ 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/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uc/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"
|
||||||
@@ -29,6 +28,8 @@ type addOptions struct {
|
|||||||
sshKey string
|
sshKey string
|
||||||
version string
|
version string
|
||||||
wgEndpoints []string
|
wgEndpoints []string
|
||||||
|
wgPort int
|
||||||
|
wgMTU int
|
||||||
yes bool
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ Connection methods:
|
|||||||
return add(cmd.Context(), uncli, remoteMachine, opts)
|
return add(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
|
"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.",
|
||||||
@@ -96,13 +98,21 @@ Connection methods:
|
|||||||
)
|
)
|
||||||
cmd.Flags().StringSliceVar(
|
cmd.Flags().StringSliceVar(
|
||||||
&opts.wgEndpoints, "wg-endpoint", nil,
|
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||||
fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+
|
"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", network.WireGuardPort)+
|
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.\n"+
|
||||||
"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]")
|
||||||
@@ -125,17 +135,26 @@ 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)
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded, uint16(opts.wgPort))
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -154,19 +173,9 @@ 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 = spinner.New().
|
err = tui.RunSpinner(ctx, "Waiting for the machine to join the cluster...", func(ctx context.Context) error {
|
||||||
Title(" Waiting for the machine to join the cluster...").
|
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
|
||||||
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,13 +7,12 @@ 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/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uc/caddy"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uc/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"
|
||||||
@@ -33,6 +32,8 @@ type initOptions struct {
|
|||||||
sshKey string
|
sshKey string
|
||||||
version string
|
version string
|
||||||
wgEndpoints []string
|
wgEndpoints []string
|
||||||
|
wgPort int
|
||||||
|
wgMTU int
|
||||||
yes bool
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,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.",
|
"Assign a name to the machine. (default is the machine's hostname)",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.network, "network", cluster.DefaultNetwork.String(),
|
&opts.network, "network", cluster.DefaultNetwork.String(),
|
||||||
@@ -135,13 +136,21 @@ Connection methods:
|
|||||||
)
|
)
|
||||||
cmd.Flags().StringSliceVar(
|
cmd.Flags().StringSliceVar(
|
||||||
&opts.wgEndpoints, "wg-endpoint", nil,
|
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||||
fmt.Sprintf("WireGuard endpoint address that other machines in the cluster should use to establish "+
|
"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", network.WireGuardPort)+
|
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.\n"+
|
||||||
"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]")
|
||||||
@@ -178,6 +187,13 @@ 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,
|
||||||
@@ -186,11 +202,13 @@ 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)
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded, uint16(opts.wgPort))
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -206,19 +224,9 @@ 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 = spinner.New().
|
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error {
|
||||||
Title(" Waiting for the cluster to be ready...").
|
return client.WaitClusterReady(ctx, 1*time.Minute)
|
||||||
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,10 +3,12 @@ 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"
|
||||||
@@ -18,14 +20,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 systemd service logs.",
|
Short: "View system service logs.",
|
||||||
Long: `View logs from the specified systemd service(s) across all machines in the cluster.
|
Long: `View logs from the specified system 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:
|
||||||
uncloud the Uncloud daemon
|
corrosion the Corrosion distributed state store
|
||||||
docker the Docker daemon
|
docker the Docker daemon
|
||||||
uncloud-corrosion the Corrosion distributed state store
|
uncloud the Uncloud daemon
|
||||||
|
|
||||||
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.
|
||||||
@@ -36,7 +38,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 uncloud-corrosion
|
uc machine logs uncloud docker 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
|
||||||
@@ -48,7 +50,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 uncloud-corrosion`,
|
uc machine logs -m machine1,machine2 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)
|
||||||
@@ -56,17 +58,19 @@ 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, units []string, opts logs.Options) error {
|
func runLogs(ctx context.Context, uncli *cli.CLI, services []string, opts logs.Options) error {
|
||||||
if len(units) == 0 {
|
if len(services) == 0 {
|
||||||
units = []string{journal.UnitUncloud}
|
services = []string{api.SystemServiceUncloud}
|
||||||
}
|
}
|
||||||
|
for _, service := range services {
|
||||||
for _, unit := range units {
|
if !slices.Contains(api.SystemServices, service) {
|
||||||
if !journal.ValidUnit(unit) {
|
return fmt.Errorf("invalid system service '%s'; valid services: %s",
|
||||||
return fmt.Errorf("invalid systemd service '%s'", unit)
|
service, strings.Join(api.SystemServices, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,27 +105,27 @@ func runLogs(ctx context.Context, uncli *cli.CLI, units []string, opts logs.Opti
|
|||||||
machineNames = append(machineNames, m.Machine.Name)
|
machineNames = append(machineNames, m.Machine.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect one log stream per unit. MachineLogs merges across machines internally.
|
// Collect one log stream per service. MachineLogs merges across machines internally.
|
||||||
unitStreams := make([]<-chan api.ServiceLogEntry, 0, len(units))
|
serviceStreams := make([]<-chan api.ServiceLogEntry, 0, len(services))
|
||||||
for _, unit := range units {
|
for _, service := range services {
|
||||||
ch, err := c.MachineLogs(ctx, unit, logsOpts)
|
ch, err := c.MachineLogs(ctx, service, logsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stream logs for systemd service '%s': %w", unit, err)
|
return fmt.Errorf("stream logs for system service '%s': %w", service, err)
|
||||||
}
|
}
|
||||||
unitStreams = append(unitStreams, ch)
|
serviceStreams = append(serviceStreams, ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stream <-chan api.ServiceLogEntry
|
var stream <-chan api.ServiceLogEntry
|
||||||
if len(unitStreams) == 1 {
|
if len(serviceStreams) == 1 {
|
||||||
stream = unitStreams[0]
|
stream = serviceStreams[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 units skips it to avoid duplicate warnings.
|
// so the outer merger across services skips it to avoid duplicate warnings.
|
||||||
merger := client.NewLogMerger(unitStreams, client.LogMergerOptions{})
|
merger := client.NewLogMerger(serviceStreams, client.LogMergerOptions{})
|
||||||
stream = merger.Stream()
|
stream = merger.Stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
formatter := logs.NewFormatter(machineNames, units, opts.UTC)
|
formatter := logs.NewFormatter(machineNames, services, opts.UTC)
|
||||||
|
|
||||||
// Print merged logs.
|
// Print merged logs.
|
||||||
for entry := range stream {
|
for entry := range stream {
|
||||||
@@ -2,10 +2,12 @@ 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"
|
||||||
@@ -13,19 +15,29 @@ 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)
|
return list(cmd.Context(), uncli, output)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) error {
|
func list(ctx context.Context, uncli *cli.CLI, output string) 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)
|
||||||
@@ -37,9 +49,19 @@ func list(ctx context.Context, uncli *cli.CLI) 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", "MACHINE ID")
|
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS",
|
||||||
|
"OS", "KERNEL", "ARCH", "DOCKER", "VERSION")
|
||||||
|
|
||||||
for _, member := range machines {
|
for _, member := range machines {
|
||||||
m := member.Machine
|
m := member.Machine
|
||||||
@@ -58,17 +80,46 @@ func list(ctx context.Context, uncli *cli.CLI) 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(", ")),
|
||||||
member.Machine.Id,
|
osName,
|
||||||
|
kernel,
|
||||||
|
arch,
|
||||||
|
dockerVersion,
|
||||||
|
daemonVersion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +63,16 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// Verify the machine exists and list all service containers on it including stopped ones.
|
// Verify the machine exists in the cluster.
|
||||||
mctx, machines, err := client.ProxyMachinesContext(ctx, []string{nameOrID})
|
member, err := client.InspectMachine(ctx, nameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("inspect machine '%s': %w", nameOrID, err)
|
||||||
}
|
}
|
||||||
if len(machines) == 0 {
|
m := member.Machine
|
||||||
return fmt.Errorf("machine '%s' not found in the cluster", nameOrID)
|
|
||||||
}
|
// Create a proxy context for the machine being removed.
|
||||||
m := machines[0].Machine
|
// This is used for calls that need to run directly on that 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)
|
||||||
@@ -103,7 +104,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(mctx, "", listOpts)
|
machineContainers, err := client.Docker.ListServiceContainers(rmCtx, "", listOpts)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
reachable = true
|
reachable = true
|
||||||
containers = machineContainers[0].Containers
|
containers = machineContainers[0].Containers
|
||||||
@@ -113,7 +114,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)
|
||||||
fmt.Println(formatContainerTree(containers))
|
lipgloss.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.")
|
||||||
@@ -150,20 +151,22 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil {
|
// Initiate reset before removing the machine from the cluster to stop it from updating the cluster store.
|
||||||
return fmt.Errorf("remove machine from cluster: %w", err)
|
// This is still optimistic as Reset only triggers the reset process that runs asynchronously.
|
||||||
}
|
|
||||||
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
|
||||||
|
|
||||||
if reset && reachable {
|
if reset && reachable {
|
||||||
_, err = client.MachineClient.Reset(mctx, &pb.ResetRequest{})
|
_, err = client.MachineClient.Reset(rmCtx, &pb.ResetRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARNING: Failed to reset machine: %v\n", err)
|
tui.PrintWarning(fmt.Sprintf("Failed to reset machine: %v\n", err))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Machine reset initiated and will complete in the background.")
|
fmt.Println("Machine reset initiated and will complete in the background.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil {
|
||||||
|
return fmt.Errorf("remove machine from cluster: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||||
|
|
||||||
// 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,6 +6,7 @@ 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"
|
||||||
@@ -38,10 +39,7 @@ 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, _, err = client.ProxyMachinesContext(ctx, nil)
|
ctx = 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 {
|
||||||
@@ -51,6 +49,15 @@ 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
|
||||||
}
|
}
|
||||||
@@ -101,7 +108,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,8 @@ 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", network.WireGuardPort)+
|
"Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is %d if omitted.\n",
|
||||||
|
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.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,16 +90,13 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// First, resolve the machine to get its ID
|
// Resolve the machine to capture its current configuration for the before/after report and to validate existence.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the update request
|
req := &pb.UpdateMachineRequest{}
|
||||||
req := &pb.UpdateMachineRequest{
|
|
||||||
MachineId: machine.Machine.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.name != "" {
|
if opts.name != "" {
|
||||||
req.Name = &opts.name
|
req.Name = &opts.name
|
||||||
@@ -121,7 +119,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)
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded, network.DefaultWireGuardPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -131,8 +129,7 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
|
|||||||
req.Endpoints = endpoints
|
req.Endpoints = endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the update operation
|
updatedMachine, err := client.UpdateMachine(ctx, machine.Machine.Id, req)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -8,17 +8,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"github.com/psviderski/uncloud/cmd/uc/caddy"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
cmdcontext "github.com/psviderski/uncloud/cmd/uc/context"
|
||||||
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
"github.com/psviderski/uncloud/cmd/uc/dns"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uc/image"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/image"
|
cmdmachine "github.com/psviderski/uncloud/cmd/uc/machine"
|
||||||
cmdmachine "github.com/psviderski/uncloud/cmd/uncloud/machine"
|
"github.com/psviderski/uncloud/cmd/uc/service"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
"github.com/psviderski/uncloud/cmd/uc/volume"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
"github.com/psviderski/uncloud/cmd/uc/wg"
|
||||||
"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,7 +39,6 @@ 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 {
|
||||||
@@ -113,15 +112,11 @@ 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",
|
||||||
urlStyle.Render("https://uncloud.run/docs"))
|
tui.URLStyle.Render(version.DocsURL))
|
||||||
fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n",
|
fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n",
|
||||||
urlStyle.Render("https://uncloud.run/discord"))
|
tui.URLStyle.Render(version.DiscordURL))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,6 +131,7 @@ func main() {
|
|||||||
NewDocsCommand(),
|
NewDocsCommand(),
|
||||||
NewImagesCommand(),
|
NewImagesCommand(),
|
||||||
NewPsCommand(),
|
NewPsCommand(),
|
||||||
|
NewProxyCommand(),
|
||||||
caddy.NewRootCommand(),
|
caddy.NewRootCommand(),
|
||||||
cmdcontext.NewRootCommand(),
|
cmdcontext.NewRootCommand(),
|
||||||
dns.NewRootCommand(),
|
dns.NewRootCommand(),
|
||||||
@@ -151,6 +147,7 @@ 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(),
|
||||||
)
|
)
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
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 ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ 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"
|
||||||
@@ -83,20 +82,10 @@ func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
|
|||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
var containers []containerInfo
|
var containers []containerInfo
|
||||||
err = spinner.New().
|
err = tui.RunSpinner(ctx, "Collecting container info...", func(ctx context.Context) error {
|
||||||
Title(" Collecting container info...").
|
containers, err = collectContainers(ctx, clusterClient)
|
||||||
Type(spinner.MiniDot).
|
return err
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -193,23 +182,12 @@ func printContainers(containers []containerInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.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, machines, err := cli.ProxyMachinesContext(ctx, nil)
|
listCtx := 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(
|
||||||
@@ -221,29 +199,17 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
|
|||||||
|
|
||||||
var containers []containerInfo
|
var containers []containerInfo
|
||||||
for _, msc := range machineContainers {
|
for _, msc := range machineContainers {
|
||||||
// Metadata can be nil if the request was broadcasted to only one machine.
|
// NOTE: Metadata should never be nil in practice. This is legacy fallback that will be removed.
|
||||||
if msc.Metadata == nil && len(machineContainers) > 1 {
|
if msc.Metadata == nil {
|
||||||
return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
tui.PrintWarning("metadata is missing in response from unknown server")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
machineName := "unknown"
|
machineName := msc.Metadata.MachineName
|
||||||
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 != nil && msc.Metadata.Error != "" {
|
if msc.Metadata.Error != "" {
|
||||||
tui.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
|
tui.PrintWarning(fmt.Sprintf("failed to list service containers on machine %s: %s",
|
||||||
msc.Metadata.Error))
|
machineName, msc.Metadata.Error))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ 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"
|
||||||
@@ -54,15 +55,6 @@ 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)
|
||||||
@@ -97,7 +89,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 := machinesNamesByID[ctr.MachineID]
|
machine := ctr.MachineName
|
||||||
if machine == "" {
|
if machine == "" {
|
||||||
machine = ctr.MachineID
|
machine = ctr.MachineID
|
||||||
}
|
}
|
||||||
@@ -135,6 +127,6 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.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,12 +74,10 @@ 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,6 +6,7 @@ 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"
|
||||||
@@ -82,6 +83,6 @@ func list(ctx context.Context, uncli *cli.CLI) error {
|
|||||||
t.Row(row...)
|
t.Row(row...)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -93,15 +93,17 @@ 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_port:container_port[/protocol]@host\n"+
|
"Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]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")
|
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+
|
||||||
|
" -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.IsStdinTerminal() {
|
if !tui.IsTerminalAvailable() {
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ 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"
|
||||||
@@ -95,6 +96,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ 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"
|
||||||
@@ -60,10 +61,7 @@ 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, _, err = client.ProxyMachinesContext(ctx, []string{opts.machine})
|
ctx = client.ProxySingleMachineContext(ctx, opts.machine)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
|
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
|
||||||
@@ -136,6 +134,6 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,10 @@ 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.",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Aliases: []string{"remove", "delete"},
|
||||||
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,441 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
_ _ _
|
||||||
|
_ _ _ __ ___| | ___ _ _ __| | __| |
|
||||||
|
| | | | '_ \ / __| |/ _ \| | | |/ _` |/ _` |
|
||||||
|
| |_| | | | | (__| | (_) | |_| | (_| | (_| |
|
||||||
|
\__,_|_| |_|\___|_|\___/ \__,_|\__,_|\__,_|
|
||||||
@@ -7,9 +7,11 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -24,7 +26,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloudd",
|
Use: "uncloudd",
|
||||||
Short: "Uncloud machine daemon.",
|
Short: "Uncloud machine daemon.",
|
||||||
Version: version.String(),
|
Long: "Uncloud machine daemon.\n" + tui.URLStyle.Render(version.WebsiteURL),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -39,11 +41,13 @@ 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())
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ func TestDiscovery() error {
|
|||||||
}
|
}
|
||||||
endpoints[i] = &pb.Endpoint{
|
endpoints[i] = &pb.Endpoint{
|
||||||
Ip: ip,
|
Ip: ip,
|
||||||
Port: network.WireGuardPort,
|
Port: network.DefaultWireGuardPort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = client.SetLocalData(
|
if err = client.SetLocalData(
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ 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.2
|
github.com/charmbracelet/colorprofile v0.4.3
|
||||||
github.com/charmbracelet/x/ansi v0.11.6
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
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
|
||||||
@@ -29,12 +30,15 @@ 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
|
||||||
@@ -44,8 +48,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.19.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.45.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
|
||||||
@@ -94,7 +98,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-20260205113103-524a6607adb8 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // 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
|
||||||
@@ -164,7 +168,6 @@ 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
|
||||||
@@ -199,13 +202,12 @@ 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.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.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.20 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // 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
|
||||||
@@ -246,7 +248,6 @@ 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
|
||||||
@@ -357,3 +358,5 @@ 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
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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=
|
||||||
@@ -138,6 +136,8 @@ 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.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.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.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/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,6 +914,8 @@ 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=
|
||||||
@@ -1108,8 +1110,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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
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=
|
||||||
@@ -1150,8 +1152,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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.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=
|
||||||
|
|||||||
+46
-12
@@ -11,6 +11,8 @@ 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"
|
||||||
@@ -174,6 +176,8 @@ 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.
|
||||||
@@ -234,6 +238,8 @@ 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() {
|
||||||
@@ -317,13 +323,15 @@ 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, error) {
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *client.Client, _ *client.Client, err error) {
|
||||||
contextName := cli.ContextOverrideOrCurrent()
|
contextName := cli.ContextOverrideOrCurrent()
|
||||||
c, err := cli.ConnectCluster(ctx)
|
c, err := cli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -344,16 +352,19 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
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.
|
||||||
// TODO: refactor to use client.InspectMachine.
|
inspectResp, err := machineClient.MachineClient.InspectMachine(ctx, nil)
|
||||||
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.
|
||||||
machines, err := c.ListMachines(ctx, nil)
|
var machines api.MachineMembersList
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -401,11 +412,32 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
} 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: opts.MachineName,
|
Name: machineName,
|
||||||
Network: &pb.NetworkConfig{
|
Network: &pb.NetworkConfig{
|
||||||
Endpoints: endpoints,
|
Endpoints: endpoints,
|
||||||
PublicKey: token.PublicKey,
|
PublicKey: token.PublicKey,
|
||||||
@@ -425,16 +457,16 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current store DB version from the cluster to pass to the join request.
|
// Snapshot the cluster store version so the new machine can catch up before participating.
|
||||||
var storeDBVersion int64
|
var storeVersion map[string]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 {
|
||||||
storeDBVersion = inspectResp.Machines[0].StoreDbVersion
|
storeVersion = inspectResp.Machines[0].StoreVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -451,9 +483,11 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
|
|
||||||
// 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,
|
||||||
MinStoreDbVersion: storeDBVersion,
|
MinStoreVersion: storeVersion,
|
||||||
|
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)
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ 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 the stdout is not a terminal, it falls back to simple progress logs to stderr.
|
// If a terminal is not available, 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 stdout is not a terminal, fall back to simple progress logs.
|
if !tui.IsTerminalAvailable() {
|
||||||
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 {
|
||||||
@@ -45,8 +44,8 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
|
|||||||
return cli, err
|
return cli, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the connection TUI model.
|
// Run the connection TUI model. Render to stderr so stdout stays clean for command output.
|
||||||
p := tea.NewProgram(newConnectModel(ctx, conn))
|
p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr))
|
||||||
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)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,18 +39,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, the default WireGuard port is used.
|
// be an IP address, IP:PORT, IPv6, or [IPv6]:PORT. If the port is omitted, defaultPort is used.
|
||||||
func ParseWireGuardEndpoints(values []string) ([]*pb.IPPort, error) {
|
func ParseWireGuardEndpoints(values []string, defaultPort uint16) ([]*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 default WireGuard port.
|
// Try parsing as a bare IP address and use the provided default 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, network.WireGuardPort)
|
ap = netip.AddrPortFrom(addr, defaultPort)
|
||||||
}
|
}
|
||||||
endpoints = append(endpoints, pb.NewIPPort(ap))
|
endpoints = append(endpoints, pb.NewIPPort(ap))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 systemd service.
|
// Service/container_id or service name for a system 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 systemd service '%s' on machine '%s'",
|
msg = fmt.Sprintf("WARNING: log stream from system service '%s' on machine '%s'",
|
||||||
entry.Metadata.ServiceName,
|
entry.Metadata.ServiceName,
|
||||||
entry.Metadata.MachineName)
|
entry.Metadata.MachineName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,15 +99,15 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func promptResetMachine() error {
|
func promptResetMachine() error {
|
||||||
if !tui.IsStdinTerminal() {
|
if !tui.IsTerminalAvailable() {
|
||||||
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.Println(tui.Red.Render("The remote machine is already initialised as a cluster member. Resetting it will:\n" +
|
fmt.Fprintln(os.Stderr, 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 machine to the uninitialised state"))
|
"- Reset the Uncloud daemon on the machine to the uninitialised state"))
|
||||||
|
|
||||||
var confirm bool
|
var confirm bool
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -119,7 +119,9 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ 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
|
||||||
}
|
}
|
||||||
@@ -51,6 +53,14 @@ 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()))
|
||||||
@@ -61,6 +71,11 @@ 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 {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ 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.
|
||||||
|
|||||||
@@ -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 is the maximum amount of time an HTTP2 client will retry a request.
|
// http2MaxRetryTime bounds the transport retry of a single request. Kept short so it only absorbs transient
|
||||||
http2MaxRetryTime = 10 * time.Second
|
// blips. Retrying during a Corrosion outage is the job of the higher-level recovery loops.
|
||||||
// resubscribeMaxRetryTime is the maximum amount of time an API client will retry resubscribing to a query after
|
http2MaxRetryTime = 2 * time.Second
|
||||||
// an error occurs.
|
// resubscribeMaxRetryTime bounds resubscribing to a query after an error.
|
||||||
resubscribeMaxRetryTime = 60 * time.Second
|
resubscribeMaxRetryTime = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,39 +33,43 @@ type APIClient struct {
|
|||||||
newResubBackoff func() backoff.BackOff
|
newResubBackoff func() backoff.BackOff
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIClient creates a new Corrosion API client. The client retries on network errors using an exponential backoff
|
// NewAPIClient creates a new Corrosion API client. The bearerToken is sent in the Authorization header of every
|
||||||
// policy with a maximum interval of 1 second and a maximum elapsed time of 10 seconds.
|
// request to authenticate against Corrosion API.
|
||||||
// It automatically resubscribes to active subscriptions if an error occurs using an exponential backoff policy with a
|
//
|
||||||
// maximum interval of 1 second and a maximum elapsed time of 60 seconds.
|
// Retries are split by failure mode: the transport briefly retries a single request on transient network errors, while
|
||||||
|
// 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, opts ...APIClientOption) (*APIClient, error) {
|
func NewAPIClient(addr netip.AddrPort, bearerToken string, 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)
|
||||||
}
|
}
|
||||||
c := &APIClient{
|
|
||||||
baseURL: baseURL,
|
transport := &AuthRoundTripper{
|
||||||
client: &http.Client{
|
Base: &RetryRoundTripper{
|
||||||
Transport: &RetryRoundTripper{
|
Base: &http2.Transport{
|
||||||
Base: &http2.Transport{
|
AllowHTTP: true,
|
||||||
AllowHTTP: true,
|
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
dialer := &net.Dialer{
|
||||||
dialer := &net.Dialer{
|
Timeout: http2ConnectTimeout,
|
||||||
Timeout: http2ConnectTimeout,
|
}
|
||||||
}
|
return dialer.DialContext(ctx, network, addr)
|
||||||
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),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
NewBackoff: func() backoff.BackOff {
|
||||||
|
return backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxElapsedTime(http2MaxRetryTime),
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
Token: bearerToken,
|
||||||
|
}
|
||||||
|
c := &APIClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
client: &http.Client{Transport: transport},
|
||||||
newResubBackoff: func() backoff.BackOff {
|
newResubBackoff: func() backoff.BackOff {
|
||||||
return backoff.NewExponentialBackOff(
|
return backoff.NewExponentialBackOff(
|
||||||
backoff.WithInitialInterval(100*time.Millisecond),
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
@@ -83,6 +87,8 @@ func NewAPIClient(addr netip.AddrPort, opts ...APIClientOption) (*APIClient, err
|
|||||||
|
|
||||||
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
|
||||||
@@ -97,6 +103,23 @@ 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.
|
||||||
@@ -107,8 +130,7 @@ 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 {
|
||||||
var opErr *net.OpError
|
if _, ok := errors.AsType[*net.OpError](err); ok {
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ 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
|
||||||
@@ -286,10 +290,25 @@ 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) {
|
||||||
slog.Debug("Retrying to resubscribe to Corrosion query.", "id", id, "from_change", fromChange)
|
sub, err := c.ResubscribeContext(ctx, id, fromChange)
|
||||||
return c.ResubscribeContext(ctx, id, fromChange)
|
if err != nil {
|
||||||
}, c.newResubBackoff())
|
// A gone subscription can never be resubscribed, so stop retrying immediately and let the caller
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,13 +331,38 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@@ -31,8 +31,9 @@ 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.
|
||||||
MinClientVersion = "0.0.0"
|
// TODO: update to 0.20.0 before releasing 0.20.0.
|
||||||
MinServerVersion = "0.0.0"
|
MinClientVersion = "0.20.0-nightly"
|
||||||
|
MinServerVersion = "0.20.0-nightly"
|
||||||
|
|
||||||
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
|
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,31 +10,11 @@ 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 {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package journal
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,10 +12,6 @@ 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
|
||||||
|
|||||||
@@ -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{11, 0}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
@@ -339,126 +339,6 @@ 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
|
||||||
@@ -470,7 +350,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[6]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -483,7 +363,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[6]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
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 {
|
||||||
@@ -496,7 +376,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{6}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RemoveMachineRequest) GetId() string {
|
func (x *RemoveMachineRequest) GetId() string {
|
||||||
@@ -517,7 +397,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[7]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -530,7 +410,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[7]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
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 {
|
||||||
@@ -543,7 +423,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{7}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Domain) GetName() string {
|
func (x *Domain) GetName() string {
|
||||||
@@ -564,7 +444,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[8]
|
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)
|
||||||
}
|
}
|
||||||
@@ -577,7 +457,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[8]
|
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 {
|
||||||
@@ -590,7 +470,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{8}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReserveDomainRequest) GetEndpoint() string {
|
func (x *ReserveDomainRequest) GetEndpoint() string {
|
||||||
@@ -611,7 +491,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[9]
|
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)
|
||||||
}
|
}
|
||||||
@@ -624,7 +504,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[9]
|
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 {
|
||||||
@@ -637,7 +517,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{9}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
||||||
@@ -658,7 +538,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[10]
|
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)
|
||||||
}
|
}
|
||||||
@@ -671,7 +551,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[10]
|
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 {
|
||||||
@@ -684,7 +564,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{10}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
||||||
@@ -707,7 +587,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[11]
|
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)
|
||||||
}
|
}
|
||||||
@@ -720,7 +600,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[11]
|
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 {
|
||||||
@@ -733,7 +613,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{11}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DNSRecord) GetName() string {
|
func (x *DNSRecord) GetName() string {
|
||||||
@@ -797,87 +677,66 @@ 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, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
|
0x69, 0x6e, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
|
||||||
0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06,
|
||||||
0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x04,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
||||||
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65,
|
||||||
0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01,
|
||||||
0x50, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46,
|
||||||
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20,
|
0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
|
0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07,
|
||||||
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f,
|
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e,
|
||||||
0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72,
|
||||||
0x69, 0x70, 0x22, 0x43, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68,
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||||
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
||||||
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76,
|
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22,
|
||||||
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
|
||||||
0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
|
0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a,
|
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e,
|
||||||
0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
|
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09,
|
||||||
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 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, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
|
||||||
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08,
|
||||||
0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xca, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75,
|
||||||
0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
|
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
|
||||||
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
|
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
|
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70,
|
||||||
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65,
|
||||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
|
||||||
0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
|
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65,
|
||||||
0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
|
0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
|
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65,
|
||||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
|
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70,
|
||||||
0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
|
0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
|
||||||
0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x92, 0x04, 0x0a, 0x07,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d,
|
||||||
0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
|
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
|
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b,
|
||||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72,
|
||||||
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64,
|
0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
|
||||||
0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
|
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75,
|
||||||
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
|
||||||
0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75,
|
||||||
0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d,
|
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06,
|
||||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
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 (
|
||||||
@@ -893,7 +752,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, 12)
|
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||||
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
|
||||||
@@ -901,54 +760,46 @@ 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
|
||||||
(*UpdateMachineRequest)(nil), // 6: api.UpdateMachineRequest
|
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest
|
||||||
(*UpdateMachineResponse)(nil), // 7: api.UpdateMachineResponse
|
(*Domain)(nil), // 7: api.Domain
|
||||||
(*RemoveMachineRequest)(nil), // 8: api.RemoveMachineRequest
|
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest
|
||||||
(*Domain)(nil), // 9: api.Domain
|
(*CreateDomainRecordsRequest)(nil), // 9: api.CreateDomainRecordsRequest
|
||||||
(*ReserveDomainRequest)(nil), // 10: api.ReserveDomainRequest
|
(*CreateDomainRecordsResponse)(nil), // 10: api.CreateDomainRecordsResponse
|
||||||
(*CreateDomainRecordsRequest)(nil), // 11: api.CreateDomainRecordsRequest
|
(*DNSRecord)(nil), // 11: api.DNSRecord
|
||||||
(*CreateDomainRecordsResponse)(nil), // 12: api.CreateDomainRecordsResponse
|
(*NetworkConfig)(nil), // 12: api.NetworkConfig
|
||||||
(*DNSRecord)(nil), // 13: api.DNSRecord
|
(*IP)(nil), // 13: api.IP
|
||||||
(*NetworkConfig)(nil), // 14: api.NetworkConfig
|
(*MachineInfo)(nil), // 14: api.MachineInfo
|
||||||
(*IP)(nil), // 15: api.IP
|
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
||||||
(*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{
|
||||||
14, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
12, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
15, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
13, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
||||||
16, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
14, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
16, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
14, // 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
|
||||||
15, // 6: api.UpdateMachineRequest.public_ip:type_name -> api.IP
|
11, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
||||||
17, // 7: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
|
11, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
||||||
16, // 8: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
|
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
||||||
13, // 9: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
13, // 10: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
15, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
1, // 11: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
||||||
2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
||||||
18, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
15, // 13: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
||||||
6, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
|
15, // 14: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
||||||
8, // 15: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
9, // 15: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
||||||
10, // 16: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
3, // 16: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
18, // 17: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
5, // 17: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
18, // 18: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
15, // 18: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
||||||
11, // 19: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
7, // 19: api.Cluster.ReserveDomain:output_type -> api.Domain
|
||||||
3, // 20: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
7, // 20: api.Cluster.GetDomain:output_type -> api.Domain
|
||||||
5, // 21: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
7, // 21: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
||||||
7, // 22: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
|
10, // 22: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
||||||
18, // 23: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
16, // [16:23] is the sub-list for method output_type
|
||||||
9, // 24: api.Cluster.ReserveDomain:output_type -> api.Domain
|
9, // [9:16] is the sub-list for method input_type
|
||||||
9, // 25: api.Cluster.GetDomain:output_type -> api.Domain
|
9, // [9:9] is the sub-list for extension type_name
|
||||||
9, // 26: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
9, // [9:9] is the sub-list for extension extendee
|
||||||
12, // 27: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
0, // [0:9] is the sub-list for field type_name
|
||||||
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() }
|
||||||
@@ -1008,30 +859,6 @@ 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.(*UpdateMachineRequest); 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[5].Exporter = func(v any, i int) any {
|
|
||||||
switch v := v.(*UpdateMachineResponse); 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[6].Exporter = func(v any, i int) any {
|
|
||||||
switch v := v.(*RemoveMachineRequest); i {
|
switch v := v.(*RemoveMachineRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1043,7 +870,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[7].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.(*Domain); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1055,7 +882,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[8].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.(*ReserveDomainRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1067,7 +894,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[9].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.(*CreateDomainRecordsRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1079,7 +906,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[10].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.(*CreateDomainRecordsResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1091,7 +918,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[11].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.(*DNSRecord); i {
|
switch v := v.(*DNSRecord); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1104,14 +931,13 @@ 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: 12,
|
NumMessages: 10,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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);
|
||||||
@@ -51,20 +50,6 @@ 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,7 +22,6 @@ 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"
|
||||||
@@ -36,7 +35,6 @@ 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)
|
||||||
@@ -72,16 +70,6 @@ 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)
|
||||||
@@ -138,7 +126,6 @@ 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)
|
||||||
@@ -160,9 +147,6 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -235,24 +219,6 @@ 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 {
|
||||||
@@ -358,10 +324,6 @@ 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,
|
||||||
|
|||||||
@@ -81,8 +81,12 @@ 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.
|
||||||
Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
MachineAddr string `protobuf:"bytes,1,opt,name=machine_addr,json=machineAddr,proto3" json:"machine_addr,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.
|
||||||
@@ -121,9 +125,23 @@ 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) GetMachine() string {
|
func (x *Metadata) GetMachineId() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Machine
|
return x.MachineId
|
||||||
|
}
|
||||||
|
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 ""
|
||||||
}
|
}
|
||||||
@@ -549,53 +567,58 @@ 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, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb1, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||||
0x61, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01,
|
0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64,
|
||||||
0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49,
|
||||||
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
|
0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
|
||||||
0x72, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
|
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x0b, 0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53,
|
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
|
||||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a,
|
0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
0x69, 0x6e, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
|
||||||
0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a,
|
||||||
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
|
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||||
0x61, 0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75,
|
||||||
0x73, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
|
0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x45, 0x6d, 0x70,
|
||||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,
|
||||||
0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||||
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
|
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a,
|
||||||
0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70,
|
0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
|
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
|
0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x08, 0x6d, 0x65,
|
||||||
0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65,
|
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02,
|
||||||
0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x35, 0x0a, 0x06,
|
||||||
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04,
|
0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73,
|
0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12,
|
||||||
0x22, 0x75, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12,
|
||||||
0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
|
0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70,
|
||||||
0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18,
|
0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x22, 0x75, 0x0a, 0x0b,
|
||||||
0x69, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63,
|
0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||||
0x65, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
|
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66,
|
||||||
0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45,
|
0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c,
|
||||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01,
|
0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e,
|
0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65,
|
||||||
0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06,
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a,
|
||||||
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e,
|
||||||
0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
|
0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
|
||||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
|
0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e,
|
||||||
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65,
|
||||||
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74,
|
0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18,
|
||||||
0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a,
|
0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07,
|
||||||
0x09, 0x48, 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 0x42, 0x37, 0x5a, 0x35,
|
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d,
|
||||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
|
||||||
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
|
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
|
||||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
|
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a,
|
||||||
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x48, 0x45, 0x41,
|
||||||
|
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 (
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ 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 = 1;
|
string machine_addr = 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
@@ -19,6 +19,8 @@ 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.
|
||||||
@@ -34,6 +36,19 @@ 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 {
|
||||||
@@ -43,6 +58,17 @@ 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;
|
||||||
@@ -61,6 +87,10 @@ 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 {
|
||||||
@@ -70,8 +100,13 @@ message InitClusterResponse {
|
|||||||
message JoinClusterRequest {
|
message JoinClusterRequest {
|
||||||
MachineInfo machine = 1;
|
MachineInfo machine = 1;
|
||||||
repeated MachineInfo other_machines = 3;
|
repeated MachineInfo other_machines = 3;
|
||||||
// Minimum store database version the new machine should sync to before starting cluster operations.
|
// WireGuard listen port for this machine. Uses the default port (51820) if 0 or not set.
|
||||||
int64 min_store_db_version = 4;
|
int32 wireguard_port = 5;
|
||||||
|
// 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 {
|
||||||
@@ -82,10 +117,11 @@ 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,6 +26,7 @@ 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"
|
||||||
@@ -45,6 +46,8 @@ 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.
|
||||||
@@ -121,6 +124,16 @@ 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)
|
||||||
@@ -183,6 +196,8 @@ 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.
|
||||||
@@ -217,6 +232,9 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -358,6 +376,24 @@ 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 {
|
||||||
@@ -454,6 +490,10 @@ 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,
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
// MetadataBackend wraps a proxy.Backend and injects machine metadata into responses in One2Many mode.
|
||||||
// successful and failure responses might be returned in One2Many mode.
|
type MetadataBackend struct {
|
||||||
type One2ManyResponder struct {
|
proxy.Backend
|
||||||
machine string
|
MachineID 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.
|
||||||
@@ -59,10 +62,12 @@ type One2ManyResponder 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 *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
|
func (b *MetadataBackend) 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{
|
||||||
Machine: b.machine,
|
MachineAddr: b.MachineAddr,
|
||||||
|
MachineId: b.MachineID,
|
||||||
|
MachineName: b.MachineName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,12 +129,14 @@ func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, err
|
|||||||
//
|
//
|
||||||
// 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 *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
|
func (b *MetadataBackend) 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{
|
||||||
Machine: b.machine,
|
MachineAddr: b.MachineAddr,
|
||||||
Error: err.Error(),
|
MachineId: b.MachineID,
|
||||||
Status: status.Convert(err).Proto(),
|
MachineName: b.MachineName,
|
||||||
|
Error: err.Error(),
|
||||||
|
Status: status.Convert(err).Proto(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ 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"
|
||||||
@@ -15,27 +18,22 @@ type Director struct {
|
|||||||
localBackend *LocalBackend
|
localBackend *LocalBackend
|
||||||
remotePort uint16
|
remotePort uint16
|
||||||
remoteBackends sync.Map
|
remoteBackends sync.Map
|
||||||
// mu synchronizes access to localAddress.
|
localAddress atomic.Value
|
||||||
mu sync.RWMutex
|
mapper MachineMapper
|
||||||
localAddress string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirector(localSockPath string, remotePort uint16) *Director {
|
func NewDirector(localSockPath string, remotePort uint16, mapper MachineMapper) *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.
|
// to the local gRPC server. It is called once during machine startup before the proxy server accepts requests.
|
||||||
func (d *Director) UpdateLocalAddress(addr string) {
|
func (d *Director) UpdateLocalAddress(addr string) {
|
||||||
d.mu.Lock()
|
d.localAddress.Store(addr)
|
||||||
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
|
||||||
@@ -51,39 +49,90 @@ 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, ok := md["machines"]
|
machines, hasMachines := md["machines"]
|
||||||
if !ok {
|
machine, hasMachine := md["machine"]
|
||||||
|
if !hasMachines && !hasMachine {
|
||||||
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
}
|
}
|
||||||
if len(machines) == 0 {
|
|
||||||
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mu.RLock()
|
// Handle singular "machine" case (One2One, no metadata injection)
|
||||||
localAddress := d.localAddress
|
if hasMachine {
|
||||||
localBackend := d.localBackend
|
if len(machine) != 1 {
|
||||||
d.mu.RUnlock()
|
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
|
||||||
|
"proxy metadata 'machine' must have exactly one value")
|
||||||
backends := make([]proxy.Backend, len(machines))
|
}
|
||||||
for i, addr := range machines {
|
if hasMachines {
|
||||||
if addr == localAddress {
|
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
|
||||||
backends[i] = localBackend
|
"both 'machine' and 'machines' proxy metadata are set")
|
||||||
continue
|
}
|
||||||
|
targets, err := d.mapper.MapMachines(ctx, machine)
|
||||||
|
if err != nil {
|
||||||
|
return proxy.One2One, nil, mapErrorToStatus(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
backend, err := d.remoteBackend(addr)
|
backend, err := d.getBackend(targets[0].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
|
|
||||||
|
// For One2One, we don't wrap in MetadataBackend as we don't inject metadata.
|
||||||
|
return proxy.One2One, []proxy.Backend{backend}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(backends) == 1 {
|
// Handle plural "machines" case (One2Many, always metadata injection)
|
||||||
return proxy.One2One, backends, nil
|
if len(machines) == 0 {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "proxy metadata 'machines' is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targets, err := d.mapper.MapMachines(ctx, machines)
|
||||||
|
if err != nil {
|
||||||
|
return proxy.One2One, nil, mapErrorToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backends := make([]proxy.Backend, len(targets))
|
||||||
|
for i, t := range targets {
|
||||||
|
backend, err := d.getBackend(t.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)?
|
||||||
|
// IIRC the proxy will try to reconnect to them indefinitely. This can be stopped by restarting the daemon.
|
||||||
|
// 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)
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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,9 +10,8 @@ import (
|
|||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
|
// LocalBackend is a proxy.Backend 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
|
||||||
@@ -21,20 +20,15 @@ type LocalBackend struct {
|
|||||||
|
|
||||||
var _ proxy.Backend = (*LocalBackend)(nil)
|
var _ proxy.Backend = (*LocalBackend)(nil)
|
||||||
|
|
||||||
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
|
// NewLocalBackend returns a new LocalBackend for the given Unix socket path.
|
||||||
// of the current machine which could be empty if it's not known. The address is used to populate response metadata
|
func NewLocalBackend(sockPath string) *LocalBackend {
|
||||||
// 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 b.machine
|
return "unix://" + b.sockPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -64,6 +58,16 @@ 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()
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,13 +14,11 @@ import (
|
|||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
|
// RemoteBackend is a proxy.Backend implementation that proxies to a remote gRPC server.
|
||||||
// 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
|
||||||
@@ -37,15 +35,12 @@ 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.machine
|
return b.target
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnection returns a gRPC connection to the remote server.
|
// GetConnection returns a gRPC connection to the remote server.
|
||||||
@@ -58,6 +53,7 @@ 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)
|
||||||
|
|
||||||
@@ -100,6 +96,16 @@ 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()
|
||||||
|
|||||||
@@ -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("containers subscription failed")
|
return fmt.Errorf("subscription to container changes in cluster store failed")
|
||||||
}
|
}
|
||||||
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
|
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
|
||||||
|
|
||||||
|
|||||||
+301
-164
@@ -15,32 +15,48 @@ 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 {
|
||||||
state *State
|
// machine is the parent machine.
|
||||||
store *store.Store
|
machine *Machine
|
||||||
|
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
|
||||||
dockerCtrl *docker.Controller
|
// corrosionDir is the disk path that holds the Corrosion config and data.
|
||||||
|
// 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
|
||||||
@@ -51,15 +67,18 @@ 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(
|
||||||
state *State,
|
machine *Machine,
|
||||||
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{},
|
||||||
@@ -67,6 +86,7 @@ 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()
|
||||||
@@ -76,27 +96,33 @@ func newClusterController(
|
|||||||
endpointChanges := wgnet.WatchEndpoints()
|
endpointChanges := wgnet.WatchEndpoints()
|
||||||
|
|
||||||
return &clusterController{
|
return &clusterController{
|
||||||
state: state,
|
machine: machine,
|
||||||
store: store,
|
state: machine.state,
|
||||||
wgnet: wgnet,
|
store: store,
|
||||||
endpointChanges: endpointChanges,
|
wgnet: wgnet,
|
||||||
server: server,
|
endpointChanges: endpointChanges,
|
||||||
corroService: corroService,
|
server: server,
|
||||||
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
corroService: corroService,
|
||||||
dockerReady: dockerReady,
|
corrosionDir: corrosionDir,
|
||||||
clusterReady: clusterReady,
|
dockerService: dockerService,
|
||||||
caddyconfigCtrl: caddyfileCtrl,
|
dockerCtrl: docker.NewController(machine.state.ID, dockerService, store),
|
||||||
dnsServer: dnsServer,
|
dockerReady: dockerReady,
|
||||||
dnsResolver: dnsResolver,
|
syncMachineTrigger: make(chan struct{}, 1),
|
||||||
unregistry: unregistry,
|
clusterReady: clusterReady,
|
||||||
stopped: make(chan struct{}),
|
caddyconfigCtrl: caddyfileCtrl,
|
||||||
|
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)); err != nil {
|
if err := firewall.ConfigureIptablesChains(network.MachineIP(cc.state.Network.Subnet),
|
||||||
|
cc.state.Network.EffectiveWireGuardPort()); err != nil {
|
||||||
return fmt.Errorf("configure iptables chains: %w", err)
|
return fmt.Errorf("configure iptables chains: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +151,11 @@ 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
|
||||||
@@ -142,7 +173,9 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
// Start the network API server before waiting for the store sync so the machine is reachable on the mesh
|
||||||
|
// 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 {
|
||||||
@@ -159,19 +192,24 @@ 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.
|
||||||
cc.waitStoreSync(ctx)
|
if err = cc.waitStoreSync(ctx); err != nil {
|
||||||
|
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()
|
||||||
err := errGroup.Wait()
|
|
||||||
if corroErr := cc.stopCorrosion(); corroErr != nil {
|
|
||||||
err = errors.Join(err, corroErr)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting metrics server.")
|
||||||
|
if err := cc.metricsServer.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("metrics server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting embedded DNS resolver.")
|
slog.Info("Starting embedded DNS resolver.")
|
||||||
if err := cc.dnsResolver.Run(ctx); err != nil {
|
if err := cc.dnsResolver.Run(ctx); err != nil {
|
||||||
@@ -189,6 +227,11 @@ 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.")
|
||||||
@@ -243,15 +286,9 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
slog.Info("Unregistry server stopped.")
|
slog.Info("Unregistry server stopped.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all controllers to finish.
|
// Wait for all controllers to finish. The Corrosion service shutdown is handled by the machine after stopping all
|
||||||
err = errGroup.Wait()
|
// local API servers that may still serve requests depending on the store.
|
||||||
|
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.
|
||||||
@@ -277,19 +314,6 @@ 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 {
|
||||||
@@ -299,6 +323,7 @@ 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)
|
||||||
@@ -341,101 +366,114 @@ func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitStoreSync waits for the store database to sync to the minimum required DB version if set in the machine state.
|
// waitStoreSync blocks until the local store version >= state.MinStoreVersion and any known gaps are synced.
|
||||||
// Blocks until synced or context is cancelled.
|
// No-op when MinStoreVersion is empty. Clears state.MinStoreVersion when reached.
|
||||||
func (cc *clusterController) waitStoreSync(ctx context.Context) {
|
func (cc *clusterController) waitStoreSync(ctx context.Context) error {
|
||||||
minVersion := cc.state.MinStoreDBVersion
|
target := cc.state.MinStoreVersion
|
||||||
if minVersion == 0 {
|
if len(target) == 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
|
slog.Info("Waiting for the initial cluster store sync.", "actors", len(target))
|
||||||
|
|
||||||
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 (
|
||||||
lastVersion int64
|
lastLagging int
|
||||||
lastLogTime time.Time
|
|
||||||
lastErrLogTime time.Time
|
lastErrLogTime time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return nil
|
||||||
|
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:
|
||||||
version, err := cc.store.DBVersion(ctx)
|
local, err := cc.store.Version(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log errors at most once every 5 seconds.
|
// Throttle error logs to 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 DB version, retrying.", "err", err)
|
slog.Error("Failed to get the cluster store version, retrying.", "err", err)
|
||||||
lastErrLogTime = time.Now()
|
lastErrLogTime = time.Now()
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if version >= minVersion {
|
lagging := laggingActors(local, target)
|
||||||
// Clear MinStoreDBVersion so next restart doesn't wait for sync.
|
if len(lagging) == 0 {
|
||||||
|
// 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.MinStoreDBVersion = 0
|
cc.state.MinStoreVersion = nil
|
||||||
if err := cc.state.Save(); err != nil {
|
err = cc.state.Save()
|
||||||
slog.Error("Failed to save machine state after the initial cluster store sync.", "err", err)
|
|
||||||
}
|
|
||||||
cc.state.mu.Unlock()
|
cc.state.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
// Wait for all known missing changes to be synced before returning.
|
return fmt.Errorf("save machine state after the initial cluster store sync: %w", err)
|
||||||
// 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
|
slog.Info("Cluster store completed the initial sync.", "actors", len(target))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log progress only once a second.
|
if len(lagging) != lastLagging {
|
||||||
if version != lastVersion && time.Since(lastLogTime) >= 1*time.Second {
|
slog.Info("Syncing cluster store.", "lagging_actors", lagging)
|
||||||
slog.Info("Syncing cluster store.", "version", version, "min_version", minVersion)
|
lastLagging = len(lagging)
|
||||||
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) {
|
func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) error {
|
||||||
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
|
return nil
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
changes, err := cc.store.KnownMissingChanges(ctx)
|
changes, err := cc.store.KnownMissingChanges(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to get known missing changes from the cluster store, skipping check.",
|
return fmt.Errorf("query known missing changes from cluster store: %w", err)
|
||||||
"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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
@@ -444,10 +482,131 @@ func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
// Retry to watch and sync containers until the context is done.
|
// Supervise the watch-and-sync pipeline 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),
|
||||||
@@ -461,7 +620,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 errors.Is(err, context.Canceled) {
|
if ctx.Err() != nil {
|
||||||
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)
|
||||||
@@ -471,74 +630,52 @@ 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.
|
// when changes occur. It returns an error when the subscription fails.
|
||||||
func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
||||||
for {
|
machines, changes, err := cc.store.SubscribeMachines(ctx)
|
||||||
// Retry to subscribe to machine changes indefinitely until the context is done.
|
if err != nil {
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
return fmt.Errorf("subscribe to machine changes: %w", err)
|
||||||
backoff.WithInitialInterval(1*time.Second),
|
}
|
||||||
backoff.WithMaxInterval(60*time.Second),
|
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
|
||||||
backoff.WithMaxElapsedTime(0),
|
|
||||||
), ctx)
|
|
||||||
|
|
||||||
var (
|
// Assume the initial store synchronization when this machine first joined the cluster has already been completed.
|
||||||
machines []*pb.MachineInfo
|
// So the machines should not be empty. But we still have a safety check to not reconfigure with an empty list,
|
||||||
changes <-chan struct{}
|
// which would remove all peers and lock this machine out of the cluster.
|
||||||
err error
|
// A list containing only this machine is a valid state (e.g. all other machines were removed) and should still
|
||||||
)
|
// trigger reconfiguration to drop any stale peers.
|
||||||
subscribe := func() error {
|
if len(machines) > 0 {
|
||||||
if machines, changes, err = cc.store.SubscribeMachines(ctx); err != nil {
|
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
||||||
slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
|
if err = cc.configurePeers(machines); err != nil {
|
||||||
}
|
slog.Error("Failed to configure peers.", "err", err)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = backoff.Retry(subscribe, boff); err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
slog.Error("Unexpected error while retrying to subscribe to machine changes.", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
|
|
||||||
|
|
||||||
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
|
||||||
// completes. Skip configuration now and apply it when the store changes are received.
|
|
||||||
// TODO: remove this check after ensuring the store is actually synced to the latest known state at this point.
|
|
||||||
// See TODO in waitStoreSync.
|
|
||||||
if len(machines) > 0 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For simplicity, reconfigure all peers on any change. The subscription closes the changes channel both on context
|
||||||
|
// cancellation and when the subscription fails, so this loop exits in both cases.
|
||||||
|
for range 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
|
||||||
|
}
|
||||||
|
// A safety check for the exceptional case when something bad happened with the store. 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The changes channel was closed. It's a clean shutdown if the context was cancelled, otherwise the subscription
|
||||||
|
// failed and we return an error to fail the controller.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -195,89 +195,6 @@ 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 {
|
||||||
@@ -334,6 +251,13 @@ 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
Reference in New Issue
Block a user