Compare commits

..
Author SHA1 Message Date
Pasha Sviderski 2e585d0183 feat: add --recreate flag for deploy command to force container recreation 2025-08-07 18:09:35 +10:00
Pasha Sviderski ae9f943404 chore: change default restart policy for service containers always -> unless-stopped 2025-08-07 18:09:35 +10:00
Anton Ovchinnikov 8805178a58 doc: Add Sentry to sponsors 2025-08-07 00:03:38 +02:00
Pasha Sviderski ec3de3a099 feat: ask whether to reset already initialised machine on 'machine init/add' 2025-08-06 16:57:09 +10:00
Pasha Sviderski 2c02139369 fix: add ssh_key_path for connections in uncloud config only when using SSH key explicitly (not SSH agent) 2025-08-06 16:49:57 +10:00
Pasha Sviderski 6c244bb8f9 fix: do not try to reset machine when removing unreachable machine 2025-08-06 15:11:27 +10:00
Pasha Sviderski fc0bf4a91b chore: lint 2025-08-05 19:37:12 +10:00
Pasha Sviderski bc577fe405 docs: emphasize passwordless sudo in requirements 2025-08-05 19:32:11 +10:00
Pasha Sviderski 6cc0611d75 chore: meaningful error message when passwordless sudo required on machine provisioning 2025-08-05 18:56:14 +10:00
Pasha Sviderski 8beb9e2679 chore: lint 2025-08-04 19:20:52 +10:00
Pasha Sviderski 0c2ff499c6 fix: log message deleting iptables chain 2025-08-04 19:16:54 +10:00
Pasha Sviderski b39585df4c chore: do not masquerade container IPs when communicating via WG mesh 2025-08-04 19:09:38 +10:00
Pasha Sviderski 35d0a90125 fix: add machine with UFW firewall (allow ipv6 management traffic) fixes #65 2025-08-04 16:50:18 +10:00
Pasha Sviderski a7273c8c96 chore: fix wide svg rendering on landing 2025-08-04 15:22:05 +10:00
Pasha Sviderski 9b48003a3f blog: set diagram as og image for wireguard overlay post 2025-08-04 15:19:27 +10:00
Pasha Sviderski 1e750cc7e1 chore: add Blog links on landing page, fix header on small screens 2025-08-04 14:50:56 +10:00
Pasha Sviderski 2d73b541c5 blog: proof read WireGuard overlay post, update blog description tag 2025-08-04 14:21:48 +10:00
Pasha Sviderski cd9c0a1d2c docs: enable blog in docusaurus, add first wireguard overlay blog post 2025-08-01 21:45:50 +10:00
Pasha Sviderski 37a76809ec docs: wwizzle breadcrumbs and overwrite base url to /docs 2025-07-31 19:07:12 +10:00
Pasha Sviderski f6062cf343 docs: fix docs urls for local dev 2025-07-31 19:04:24 +10:00
Pasha Sviderski 4fe360c573 docs: combine landing page and docusaurus together and deploy as one image 2025-07-31 18:43:06 +10:00
Pasha Sviderski cac1550b7d docs: enable blog in docusaurus and change domain to uncloud.run 2025-07-30 21:28:06 +10:00
Pasha Sviderski 0bd42b2fca chore: do not allow to remove a machine the client is connected to, add --no-reset flag 2025-07-29 19:54:16 +10:00
Pasha Sviderski ad83d942e1 docs: manual cli upgrade 2025-07-28 21:45:33 +10:00
Pasha Sviderski 65f5a714dd feat: machine rm command to remove a machine from the cluster and reset it 2025-07-25 19:32:02 +10:00
Pasha Sviderski 2714587ec5 feat: add RemoveMachine API endpoint to remove a machine from the cluster 2025-07-25 19:30:59 +10:00
Pasha Sviderski 4166474ee8 fix: race on cluster init by waiting for corrosion service to become ready with schema applied 2025-07-25 16:28:32 +10:00
Pasha Sviderski 3faaac4da7 chore: run machine tests in parallel 2025-07-25 13:56:35 +10:00
Pasha Sviderski 1b6130447f chore: use a new uncancelled context for stopping corrosion service 2025-07-25 13:03:20 +10:00
Pasha Sviderski 614212a24c chore: clean up custom iptables chains on machine reset 2025-07-25 12:37:54 +10:00
Pasha Sviderski 3ab708a437 fix: graceful machine shutdown when not initialised 2025-07-25 10:50:23 +10:00
Pasha Sviderski 7a6c5bf6d7 feat: machine Reset endpoint with asynchronous resource and data cleanup 2025-07-24 19:50:25 +10:00
Pasha Sviderski 2acfafe218 chore: regenerate proto 2025-07-24 19:34:05 +10:00
Pasha Sviderski b24b55c4c7 chore: update api.MachineClient interface 2025-07-24 14:25:09 +10:00
Pasha Sviderski 9a88e914f4 chore: simplify cluster controller initialisation, prepare for reset 2025-07-24 14:23:33 +10:00
Pasha Sviderski f613d3ce6d chore: reformat code 2025-07-22 18:53:05 +10:00
Evgenii OrlovandGitHub 2c3bea64e5 feat: impl of functionality for renaming and updating machines (#91) 2025-07-22 18:45:19 +10:00
da3634b690 E2E tests use repeated strings for test data where constants add no value (#97)
---------

Co-authored-by: Pasha Sviderski <me@psviderski.name>
Co-authored-by: Anton Ovchinnikov <anton@tonyo.info>
2025-07-21 19:30:02 +02:00
Pasha Sviderski 6fb07db4b2 feat: follow-up compose 'ports' support: use ingress mode by default (closes #81) 2025-07-21 16:56:52 +10:00
Evgenii OrlovandGitHub fea7edcbc5 feat: add support for standard compose ports directive (#95) 2025-07-21 15:24:37 +10:00
Anton OvchinnikovandGitHub a54555cd13 feat(deploy): Recreate container when pull_policy set to Always (#100) 2025-07-21 14:47:38 +10:00
Pasha Sviderski 87c7889c2e docs: format debian installation section 2025-07-18 21:59:54 +10:00
Anton Ovchinnikov 05d0078451 chore: Enable more linters 2025-07-14 00:32:26 +02:00
Anton Ovchinnikov 053d73048c feat: Basic linting config 2025-07-13 23:50:27 +02:00
Matías García IsaíaandGitHub 4ec3e97a54 docs: add documentation regarding --no-dns flag (#93) 2025-07-11 11:59:25 +10:00
Evgenii OrlovandGitHub 10bbe9fbc5 fix: prevent race condition with Docker network creation (#89) 2025-07-10 17:51:08 +10:00
Evgenii OrlovandGitHub 31cd4c77e9 feat: support x-machines placement constraints in compose files (#90) 2025-07-10 16:46:29 +10:00
Pasha Sviderski 492a0af2b2 fix: migration script for uncloud Docker network to be compatible with Docker 28.2.0+ 2025-07-09 14:04:21 +10:00
Pasha Sviderski ff7bb25f2c fix: custom version handling in install.sh script 2025-07-09 13:22:21 +10:00
Pasha Sviderski a73cbfd691 chore: hide incomplete machine rm command from help output 2025-07-09 11:28:14 +10:00
Pasha Sviderski 55773e92ce fix: allow direct routing from WireGuard mesh to containers for Docker 28.2.0+ 2025-07-08 21:20:49 +10:00
Pasha SviderskiandGitHub 37f543b905 docs: fix suddenly exploded width of uc logo in README.md 2025-07-07 11:36:55 +10:00
Dario GriffoandGitHub e5f96509bf docs: add unofficial Debian package repository (#84) 2025-07-05 19:40:36 +10:00
Anton Ovchinnikov 2f35ac6498 chore: Add .editorconfig 2025-07-04 19:16:40 +02:00
Pasha Sviderski f75afa0cec Merge branch 'pasha/machine-rm' 2025-07-03 21:30:46 +10:00
Pasha Sviderski 0f38b128fa chore: fail 'machine rm' as not fully implemented 2025-07-03 21:30:09 +10:00
Pasha Sviderski 535b91fe52 feat: add stub for Reset method in machine API 2025-07-03 21:28:05 +10:00
Pasha Sviderski 5c8beb8bd3 chore: tidy up go.mod, remove toolchain 2025-07-03 20:57:56 +10:00
Pasha Sviderski 2b4da1e6fe chore: remove anonymous volumes created by service containers when removing them 2025-07-02 11:16:00 +10:00
Pasha Sviderski be8b4f079f feat: remove containers on machine before removing it 2025-07-02 11:14:31 +10:00
166 changed files with 23996 additions and 20004 deletions
+20
View File
@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://editorconfig.org
root = true
# Default settings for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Settings for Go files
[*.go]
indent_style = tab
indent_size = 4
# Settings for Bash scripts
[*.sh]
indent_style = space
indent_size = 4
+3 -1
View File
@@ -37,7 +37,9 @@ jobs:
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
- name: Run tests
run: make test
run: |
make ucind-image
make test
timeout-minutes: 10
check-protobuf:
+39
View File
@@ -0,0 +1,39 @@
name: Lint
on:
push:
branches:
- "main"
- "test/**"
- "release/**"
pull_request:
branches:
- main
paths:
- "**.go"
- "go.*"
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "1.23.2"
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
version: v2.2.2
- name: Format code
run: |
make format
git diff --exit-code ||
(echo "Code is not formatted. Please run 'make format' and commit the changes." && exit 1)
timeout-minutes: 10
+42
View File
@@ -0,0 +1,42 @@
version: "2"
run:
concurrency: 4
tests: true
timeout: 5m
linters:
default: none
enable:
- bodyclose
# - dogsled
- dupl
# - errcheck
- gochecknoinits
- goconst
# - gocritic
# - gocyclo
# - godot
# - gosec
# - govet
- ineffassign
- misspell
- nakedret
# - prealloc
# - revive
# - staticcheck
- unconvert
# - unparam
# - unused
- whitespace
exclusions:
rules:
- path: ^test/e2e
linters:
- goconst # constants here add no value, so we skip goconst only for test/e2e.
formatters:
enable:
- gofumpt
- goimports
+8
View File
@@ -6,6 +6,14 @@ backend = "core:go"
"go1.23.10.darwin-arm64.tar.gz" = "sha256:25c64bfa8a8fd8e7f62fb54afa4354af8409a4bb2358c2699a1003b733e6fce5"
"go1.23.10.linux-amd64.tar.gz" = "sha256:535f9f81802499f2a7dbfa70abb8fda3793725fcc29460f719815f6e10b5fd60"
[tools.golangci-lint]
version = "2.2.2"
backend = "aqua:golangci/golangci-lint"
[tools.golangci-lint.checksums]
"golangci-lint-2.2.2-darwin-arm64.tar.gz" = "sha256:d84d94d042c0d495fd1746f3d18948a75de163b17a14e8de3ef840928dd2df74"
"golangci-lint-2.2.2-linux-amd64.tar.gz" = "sha256:c27fbde948a87d326feacd21df2f61a9c54dbd2e3bfa185c0a1cd6917a6f964f"
[tools.protoc]
version = "27.3"
backend = "aqua:protocolbuffers/protobuf/protoc"
+1
View File
@@ -3,6 +3,7 @@ experimental = true
[tools]
go = "1.23"
golangci-lint = "2.2.2"
protoc = "27.3"
protoc-gen-go = "1.34.2"
protoc-gen-go-grpc = "1.5.1"
+24 -5
View File
@@ -1,6 +1,5 @@
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
DOCS_IMAGE ?= ghcr.io/psviderski/uncloud-docs:latest
update-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -46,6 +45,11 @@ proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
.PHONY: proto-mise
proto-mise:
mise exec -- protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
.PHONY: corrosion-image
corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
@@ -67,9 +71,13 @@ test:
ifeq ($(TEST_NAME),)
go test -count=1 -v ./...
else
go test -count=1 -v -run ^$(TEST_NAME)$$ ./...
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
endif
.PHONY: test-e2e
test-e2e:
go test -race -count=1 -v ./test/e2e
.PHONY: test-clean
test-clean:
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
@@ -89,6 +97,17 @@ test-clean:
vet:
go vet ./...
.PHONY: docs-image-push
docs-image:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs
.PHONY: format fmt
format fmt:
GOOS=linux golangci-lint fmt
LINT_TARGETS := lint lint-and-fix
.PHONY: $(LINT_TARGETS) _lint
$(LINT_TARGETS): _lint
lint: ARGS=
lint-and-fix: ARGS=--fix
_lint:
# Explicitly set OS to Linux to not skip *_linux.go files when running on macOS.
# Uncloud daemon won't likely support OS other than Linux anytime soon, so for now we can rely on that.
GOOS=linux golangci-lint run $(ARGS)
+20 -9
View File
@@ -1,10 +1,10 @@
<div align="center">
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<img src="./website/landing/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
<h1>Uncloud</h1>
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
<p>
<a href="https://docs.uncloud.run"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
<a href="https://github.com/sponsors/psviderski"><img src="https://img.shields.io/badge/Donate-EA4AAA.svg?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Donate"></a>
@@ -52,11 +52,11 @@ complexity of Kubernetes.
## 🎬 Quick demo
The screenshot below demonstrates how I use Uncloud to deploy the [Uncloud Documentation](https://docs.uncloud.run)
website to 2 remote machines (why not?) from the [`compose.yaml`](docs/compose.yaml) file on my local machine.
The screenshot below demonstrates how I use Uncloud to deploy https://uncloud.run website to 2 remote machines from
the [`compose.yaml`](website/compose.yaml) file on my local machine.
It exposes the container port `8000/tcp` as HTTPS on the domain `docs.uncloud.run`, served by the Caddy reverse proxy on
the remote machines. All managed by Uncloud.
It exposes the container port `8000/tcp` as HTTPS on the domain `uncloud.run`, served by the Caddy reverse proxy on the
remote machines. All managed by Uncloud.
![Uncloud compose deployment demo](.github/images/compose-deploy.jpg)
@@ -97,6 +97,8 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
curl -fsS https://get.uncloud.run/install.sh | sh
```
See [Installation](https://uncloud.run/docs/getting-started/install-cli) for more options.
2. Initialise your first machine:
```bash
@@ -128,15 +130,15 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
uncloud-uninstall
```
View the [Documentation](https://docs.uncloud.run) for more information.
View the [Documentation](https://uncloud.run/docs) for more information.
## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
Check out the [design document](misc/design.md) to understand Uncloud's design philosophy and goals.
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
![Diagram: multi-provider cluster of 3 machines](website/landing/images/diagram.webp)
<details>
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
@@ -321,6 +323,15 @@ SQLite database used to share Uncloud's cluster state.
features, and be the first to know when it's ready for production use.
* Watch this repository for releases.
## 💖 Sponsors
These companies and projects are helping Uncloud with their generous sponsorship and/or services:
<!-- Sentry -->
<a href="https://sentry.io/welcome/">
<img height="100" alt="Sentry" src="https://github.com/user-attachments/assets/6c1439c0-d20d-40dc-a669-c9aa94651dfa" />
</a>
## ❤️ Contributors
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
+2 -1
View File
@@ -2,8 +2,9 @@ package cluster
import (
"fmt"
"github.com/spf13/cobra"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
+2 -1
View File
@@ -2,8 +2,9 @@ package cluster
import (
"fmt"
"github.com/spf13/cobra"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
)
func NewRemoveCommand() *cobra.Command {
+1 -1
View File
@@ -13,7 +13,7 @@ func NewRootCommand() *cobra.Command {
}
cmd.AddCommand(
NewCreateCommand(),
//NewListCommand(),
// NewListCommand(),
NewRemoveCommand(),
)
return cmd
+8 -1
View File
@@ -21,6 +21,7 @@ type deployOptions struct {
profiles []string
services []string
noBuild bool
recreate bool
context string
}
@@ -50,6 +51,8 @@ func NewDeployCommand() *cobra.Command {
"Name of the cluster context to deploy to (default is the current context)")
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
"Do not build images before deploying services. (default false)")
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
"Recreate containers even if their configuration and image haven't changed.")
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
// Could be useful to test a new version on a subset of machines before rolling out to all.
@@ -108,7 +111,11 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}
defer clusterClient.Close()
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
var strategy deploy.Strategy
if opts.recreate {
strategy = &deploy.RollingStrategy{ForceRecreate: true}
}
composeDeploy, err := compose.NewDeploymentWithStrategy(ctx, clusterClient, project, strategy)
if err != nil {
return fmt.Errorf("create compose deployment: %w", err)
}
+7 -6
View File
@@ -41,7 +41,7 @@ func NewAddCommand() *cobra.Command {
if err != nil {
return fmt.Errorf("parse remote machine: %w", err)
}
remoteMachine := cli.RemoteMachine{
remoteMachine := &cli.RemoteMachine{
User: user,
Host: host,
Port: port,
@@ -59,11 +59,12 @@ func NewAddCommand() *cobra.Command {
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
&opts.sshKey, "ssh-key", "i", "",
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
cli.DefaultSSHKeyPath),
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
@@ -77,12 +78,12 @@ func NewAddCommand() *cobra.Command {
return cmd
}
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts addOptions) error {
var publicIP *netip.Addr
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
case "", PublicIPNone:
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
+6
View File
@@ -0,0 +1,6 @@
package machine
const (
// PublicIPNone is the value used to indicate removal of public IP
PublicIPNone = "none"
)
+5 -4
View File
@@ -77,11 +77,12 @@ func NewInitCommand() *cobra.Command {
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
&opts.sshKey, "ssh-key", "i", "",
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
cli.DefaultSSHKeyPath),
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
@@ -105,7 +106,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
case "", PublicIPNone:
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
+47
View File
@@ -0,0 +1,47 @@
package machine
import (
"context"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewRenameCommand() *cobra.Command {
var contextName string
cmd := &cobra.Command{
Use: "rename OLD_NAME NEW_NAME",
Short: "Rename a machine in the cluster.",
Long: `Rename a machine in the cluster.
This command changes the name of an existing machine while preserving all other
configuration including network settings, public IP, and cluster membership.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return rename(cmd.Context(), uncli, contextName, args[0], args[1])
},
}
cmd.Flags().StringVarP(
&contextName, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func rename(ctx context.Context, uncli *cli.CLI, contextName, oldName, newName string) error {
client, err := uncli.ConnectCluster(ctx, contextName)
if err != nil {
return err
}
defer client.Close()
machine, err := client.RenameMachine(ctx, oldName, newName)
if err != nil {
return fmt.Errorf("rename machine: %w", err)
}
fmt.Printf("Machine %q renamed to %q (ID: %s)\n", oldName, machine.Name, machine.Id)
return nil
}
+130 -20
View File
@@ -2,21 +2,25 @@ package machine
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type removeOptions struct {
force bool
noReset bool
yes bool
context string
}
@@ -27,7 +31,7 @@ func NewRmCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "rm MACHINE",
Aliases: []string{"remove", "delete"},
Short: "Remove a machine from a cluster.",
Short: "Remove a machine from a cluster and reset it.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -39,11 +43,14 @@ func NewRmCommand() *cobra.Command {
"Name of the cluster context. (default is the current context)")
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Do not prompt for confirmation before removing the machine.")
cmd.Flags().BoolVar(&opts.noReset, "no-reset", false,
"Do not reset the machine after removing it from the cluster. This will leave all containers and data intact.")
return cmd
}
func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts removeOptions) error {
func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOptions) error {
// TODO: automatically choose a connection to the machine that is not being removed.
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
@@ -51,31 +58,66 @@ func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts remove
defer client.Close()
// Verify the machine exists and list all service containers on it including stopped ones.
listCtx, machines, err := api.ProxyMachinesContext(ctx, client, []string{machineName})
mctx, machines, err := api.ProxyMachinesContext(ctx, client, []string{nameOrID})
if err != nil {
return err
}
if len(machines) == 0 {
return fmt.Errorf("machine '%s' not found in the cluster", machineName)
return fmt.Errorf("machine '%s' not found in the cluster", nameOrID)
}
m := machines[0].Machine
listOpts := container.ListOptions{All: true}
machineContainers, err := client.Docker.ListServiceContainers(listCtx, "", listOpts)
// Verify if the machine being removed is the proxy machine we're connected to.
proxyMachine, err := client.MachineClient.Inspect(ctx, nil)
if err != nil {
return fmt.Errorf("list containers: %w", err)
return fmt.Errorf("inspect proxy machine: %w", err)
}
if proxyMachine.Id == m.Id {
allMachines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
if len(allMachines) > 1 {
return errors.New("cannot remove the machine you are currently connected to. " +
"Please connect to another machine in the cluster and try again. " +
"Use --connect flag or update 'connections' for the cluster context in your Uncloud config")
// It's ok to remove the proxy machine if it's the last one in the cluster.
}
}
containers := machineContainers[0].Containers
if len(containers) > 0 {
fmt.Printf("Found %d service containers on machine '%s':\n\n", len(containers), m.Name)
fmt.Println(formatContainerTree(containers))
fmt.Println()
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
"and remove it from the cluster.")
// TODO: mark the machine as being removed and unschedulable when this is possible to prevent new containers
// from being scheduled on it while the removal is in progress.
reset := !opts.noReset
var containers []api.ServiceContainer
reachable := false
if reset {
// Check if the machine is up and has service containers.
listOpts := container.ListOptions{All: true}
machineContainers, err := client.Docker.ListServiceContainers(mctx, "", listOpts)
if err == nil {
reachable = true
containers = machineContainers[0].Containers
if len(containers) > 0 {
plural := ""
if len(containers) > 1 {
plural = "s"
}
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
fmt.Println(formatContainerTree(containers))
fmt.Println()
fmt.Println("This will remove all service containers from the machine, remove it from the cluster, " +
"and reset it to the uninitialised state.")
} else {
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
fmt.Println("This will remove the machine from the cluster and reset it to the uninitialised state.")
}
} else {
fmt.Printf("This will remove machine '%s' from the cluster without resetting it as it's unreachable.\n",
m.Name)
}
} else {
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
fmt.Println("This will reset the machine to the uninitialised state and remove it from the cluster.")
fmt.Printf("This will remove machine '%s' from the cluster without resetting it.\n", m.Name)
}
if !opts.yes {
@@ -89,11 +131,36 @@ func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts remove
}
}
// TODO: 3. Remove all service containers on the machine.
// TODO: 4. Implement and call ResetMachine via Machine API to reset the machine state to uninitialised.
// TODO: 5. Remove the machine from the cluster store.
if reset && len(containers) > 0 {
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
return removeContainers(ctx, client, containers)
}, uncli.ProgressOut(), "Removing containers")
if err != nil {
return fmt.Errorf("remove containers: %w", err)
}
fmt.Println()
}
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)
if reset && reachable {
_, err = client.MachineClient.Reset(mctx, &pb.ResetRequest{})
if err != nil {
fmt.Printf("WARNING: Failed to reset machine: %v\n", err)
} else {
fmt.Println("Machine reset initiated and will complete in the background.")
}
}
// TODO: remove the connection to the machine from the uncloud config if it exists. We need a way to associate
// the machine with its connection in the config, e.g. by storing the machine name in the connection metadata.
// TODO: If Caddy was running on this machine and a cluster domain is reserved,
// let the user know that the DNS records should be updated.
return nil
}
@@ -137,3 +204,46 @@ func formatContainerTree(containers []api.ServiceContainer) string {
return strings.Join(output, "\n")
}
// removeContainers removes the given service containers from the machine.
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
if len(containers) == 0 {
return nil
}
wg := sync.WaitGroup{}
errCh := make(chan error)
for _, ctr := range containers {
wg.Add(1)
go func(c api.ServiceContainer) {
defer wg.Done()
// Gracefully stop the container before removing it.
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
if err != nil && !errors.Is(err, api.ErrNotFound) {
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
}
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
// Remove anonymous volumes created by the container.
RemoveVolumes: true,
})
if err != nil && !errors.Is(err, api.ErrNotFound) {
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
}
}(ctr)
}
go func() {
wg.Wait()
close(errCh)
}()
var err error
for e := range errCh {
err = errors.Join(err, e)
}
return err
}
+2
View File
@@ -14,7 +14,9 @@ func NewRootCommand() *cobra.Command {
NewAddCommand(),
NewInitCommand(),
NewListCommand(),
NewRenameCommand(),
NewRmCommand(),
NewUpdateCommand(),
NewTokenCommand(),
)
return cmd
+2 -1
View File
@@ -2,9 +2,10 @@ package machine
import (
"fmt"
"github.com/spf13/cobra"
"github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/machine"
"github.com/spf13/cobra"
)
type tokenOptions struct {
+128
View File
@@ -0,0 +1,128 @@
package machine
import (
"context"
"fmt"
"net/netip"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/spf13/cobra"
)
type updateOptions struct {
name string
publicIP string
context string
}
func NewUpdateCommand() *cobra.Command {
opts := updateOptions{}
cmd := &cobra.Command{
Use: "update",
Short: "Update machine configuration in the cluster.",
Long: `Update machine configuration in the cluster.
This command allows setting various machine properties including:
- Machine name (--name)
- Public IP address (--public-ip)
At least one flag must be specified to perform an update operation.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return update(cmd.Context(), uncli, cmd, opts, args[0])
},
}
cmd.Flags().StringVar(
&opts.name, "name", "",
"New name for the machine",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "",
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
)
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
// Check if at least one flag was explicitly set
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") {
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
}
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return err
}
defer client.Close()
// First, resolve the machine to get its ID
machine, err := client.InspectMachine(ctx, machineNameOrID)
if err != nil {
return fmt.Errorf("find machine: %w", err)
}
// Build the update request
req := &pb.UpdateMachineRequest{
MachineId: machine.Machine.Id,
}
if opts.name != "" {
req.Name = &opts.name
}
// Check if --public-ip flag was explicitly provided
if cmd.Flags().Changed("public-ip") {
if opts.publicIP == "" || opts.publicIP == PublicIPNone {
req.PublicIp = &pb.IP{} // Empty IP to signal removal
} else {
// Parse and validate the public IP
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("invalid public IP address %q: %w", opts.publicIP, err)
}
req.PublicIp = pb.NewIP(ip)
}
}
// Perform the update operation
updatedMachine, err := client.UpdateMachine(ctx, req)
if err != nil {
return fmt.Errorf("update machine: %w", err)
}
// Report what was changed
changes := make([]string, 0)
if opts.name != "" {
changes = append(changes, fmt.Sprintf("name: %q -> %q", machine.Machine.Name, updatedMachine.Name))
}
if cmd.Flags().Changed("public-ip") {
oldIP := PublicIPNone
if machine.Machine.PublicIp != nil {
if addr, err := machine.Machine.PublicIp.ToAddr(); err == nil {
oldIP = addr.String()
}
}
newIP := PublicIPNone
if updatedMachine.PublicIp != nil {
if addr, err := updatedMachine.PublicIp.ToAddr(); err == nil {
newIP = addr.String()
}
}
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
}
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
for _, change := range changes {
fmt.Printf(" %s\n", change)
}
return nil
}
-1
View File
@@ -9,7 +9,6 @@ import (
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
-41
View File
@@ -1,41 +0,0 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
-8
View File
@@ -1,8 +0,0 @@
services:
uncloud-docs:
image: ghcr.io/psviderski/uncloud-docs:latest
pull_policy: always
user: nobody
x-ports:
- docs.uncloud.run:8000/https
scale: 2
-18434
View File
File diff suppressed because it is too large Load Diff
-45
View File
@@ -1,45 +0,0 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/preset-classic": "3.7.0",
"@easyops-cn/docusaurus-search-local": "^0.49.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.7.0",
"@docusaurus/types": "3.7.0"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}
+3 -2
View File
@@ -3,10 +3,11 @@ package main
import (
"context"
"fmt"
"github.com/hashicorp/serf/serf"
crdt "github.com/ipfs/go-ds-crdt"
"log/slog"
"time"
"github.com/hashicorp/serf/serf"
crdt "github.com/ipfs/go-ds-crdt"
)
// Implements the Broadcaster interface.
+2 -1
View File
@@ -2,9 +2,10 @@ package main
import (
"fmt"
"github.com/dgraph-io/badger/v3"
"log"
"time"
"github.com/dgraph-io/badger/v3"
)
func customTimeEncoder(t time.Time) string {
+2 -1
View File
@@ -2,9 +2,10 @@ package main
import (
"fmt"
"github.com/ipfs/go-log/v2"
"log/slog"
"os"
"github.com/ipfs/go-log/v2"
)
// ipfsLogger is an slog logger that implements the IPFS go-log StandardLogger interface.
+2 -1
View File
@@ -2,12 +2,13 @@ package main
import (
"fmt"
"github.com/docker/docker/libnetwork/networkdb"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/docker/docker/libnetwork/networkdb"
)
func main() {
+13 -12
View File
@@ -4,6 +4,13 @@ import (
"context"
"flag"
"fmt"
"log/slog"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/hashicorp/memberlist"
"github.com/hashicorp/serf/cmd/serf/command/agent"
"github.com/hashicorp/serf/serf"
@@ -11,12 +18,6 @@ import (
badger "github.com/ipfs/go-ds-badger3"
crdt "github.com/ipfs/go-ds-crdt"
"github.com/lmittmann/tint"
"log/slog"
"net"
"os"
"os/signal"
"syscall"
"time"
)
func createSerfAgentConfig(name, bindAddr, rpcAddr, profile string) *agent.Config {
@@ -49,9 +50,9 @@ func createSerfAgent(config *agent.Config) (*agent.Agent, error) {
serfConfig.MemberlistConfig.BindAddr = bindIP
serfConfig.MemberlistConfig.BindPort = bindPort
//serfConfig.MemberlistConfig.AdvertiseAddr = advertiseIP
//serfConfig.MemberlistConfig.AdvertisePort = advertisePort
//serfConfig.MemberlistConfig.SecretKey = encryptKey
// serfConfig.MemberlistConfig.AdvertiseAddr = advertiseIP
// serfConfig.MemberlistConfig.AdvertisePort = advertisePort
// serfConfig.MemberlistConfig.SecretKey = encryptKey
serfConfig.NodeName = config.NodeName
serfConfig.Tags = config.Tags
serfConfig.SnapshotPath = config.SnapshotPath
@@ -129,7 +130,7 @@ func main() {
logger := slog.New(tint.NewHandler(os.Stdout, &tint.Options{
AddSource: true,
Level: slog.LevelDebug,
//Level: slog.LevelInfo,
// Level: slog.LevelInfo,
TimeFormat: time.RFC3339Nano,
}))
slog.SetDefault(logger)
@@ -164,7 +165,7 @@ func main() {
opts := crdt.DefaultOptions()
opts.Logger = newIPFSLogger(logger)
//opts.MultiHeadProcessing = true
// opts.MultiHeadProcessing = true
// TODO: debug why the heads count may grow on the receiving side if the event backlog is huge and the processing
// is slow.
store, err := crdt.New(localStore, ds.NewKey("/"), syncer, broadcaster, opts)
@@ -172,7 +173,7 @@ func main() {
panic(err)
}
//ticker := time.NewTicker(10 * time.Millisecond)
// ticker := time.NewTicker(10 * time.Millisecond)
ticker := time.NewTicker(3 * time.Second)
go func() {
for {
+4 -5
View File
@@ -4,18 +4,19 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/hashicorp/serf/serf"
"github.com/ipfs/boxo/datastore/dshelp"
dag "github.com/ipfs/boxo/ipld/merkledag"
"github.com/ipfs/go-cid"
ds "github.com/ipfs/go-datastore"
ipld "github.com/ipfs/go-ipld-format"
"log/slog"
)
// Implements the DAGService interface.
// TODO: implement SessionDAGService to optimize node fetching.
// TOOD: persistentSerfDAG?
// TODO: persistentSerfDAG?
type dagSyncer struct {
// Persistent storage for the nodes.
store ds.Datastore
@@ -50,8 +51,7 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
}
slog.Debug("Queried node from peers", "cid", cid, "deadline", query.Deadline())
responded := false
for !responded {
for {
select {
case resp, ok := <-query.ResponseCh():
if !ok {
@@ -63,7 +63,6 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
continue
}
slog.Debug("Received node from peer", "cid", cid, "peer", resp.From)
responded = true
query.Close()
node, err = nodeFromBytes(resp.Payload)
+4 -3
View File
@@ -6,12 +6,13 @@ import (
"crypto/cipher"
"encoding/hex"
"fmt"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/siderolabs/discovery-api/api/v1alpha1/client/pb"
discovery "github.com/siderolabs/discovery-client/pkg/client"
"go.uber.org/zap"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/machine/network"
)
const (
-2
View File
@@ -2,8 +2,6 @@ module github.com/psviderski/uncloud
go 1.23.0
toolchain go1.23.2
require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver v1.5.0
+4
View File
@@ -105,6 +105,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -153,6 +155,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
+35 -35
View File
@@ -6,8 +6,8 @@ import (
"fmt"
"net/netip"
"os"
"slices"
"github.com/charmbracelet/huh"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
@@ -22,7 +22,12 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)
const defaultContextName = "default"
const (
// DefaultSSHKeyPath is the fallback location for the SSH private key when provisioning remote machines.
// Used when no key is explicitly provided and SSH agent authentication fails.
DefaultSSHKeyPath = "~/.ssh/id_ed25519"
defaultContextName = "default"
)
type CLI struct {
Config *config.Config
@@ -173,7 +178,7 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
}
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
if err != nil {
return nil, err
}
@@ -190,7 +195,7 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
return nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
return nil, err
}
}
@@ -249,7 +254,7 @@ type AddMachineOptions struct {
Context string
MachineName string
PublicIP *netip.Addr
RemoteMachine RemoteMachine
RemoteMachine *RemoteMachine
Version string
}
@@ -272,7 +277,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
}
}()
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
if err != nil {
return nil, nil, err
}
@@ -288,7 +293,18 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
return nil, nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
// Check if the machine is already a member of this cluster.
machines, err := c.ListMachines(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
}
if slices.ContainsFunc(machines, func(m *pb.MachineMember) bool {
return m.Machine.Id == minfo.Id
}) {
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
}
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
return nil, nil, err
}
}
@@ -339,7 +355,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
}
// List other machines in the cluster to include them in the join request.
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
@@ -382,11 +398,20 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
// a machine API client to interact with the machine. The client should be closed after use by the caller.
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
func (cli *CLI) provisionRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, version string,
// The remoteMachine.SSHKeyPath could be updated to the default SSH key path if it is not set and the SSH agent
// authentication fails.
func provisionRemoteMachine(
ctx context.Context, remoteMachine *RemoteMachine, version string,
) (*client.Client, error) {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
if err != nil && remoteMachine.KeyPath == "" {
remoteMachine.KeyPath = DefaultSSHKeyPath
sshClient, err = sshexec.Connect(
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
)
}
if err != nil {
return nil, fmt.Errorf(
"SSH login to remote machine %s: %w",
@@ -420,31 +445,6 @@ func (cli *CLI) provisionRemoteMachine(
return machineClient, nil
}
func (cli *CLI) promptResetMachine() error {
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"The remote machine is already initialised as a cluster member. Do you want to reset it first?",
).
Affirmative("Yes!").
Negative("No").
Value(&confirm),
),
).WithAccessible(true)
if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err)
}
if !confirm {
return fmt.Errorf("remote machine is already initialised as a cluster member")
}
// TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
}
// ProgressOut returns an output stream for progress writer.
func (cli *CLI) ProgressOut() *streams.Out {
return streams.NewOut(os.Stdout)
+2 -2
View File
@@ -53,11 +53,11 @@ func (c *Config) Read() error {
func (c *Config) Save() error {
dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil {
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create config directory '%s': %w", dir, err)
}
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("write config file '%s': %w", c.path, err)
}
+84 -3
View File
@@ -5,12 +5,20 @@ import (
"fmt"
"os"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/charmbracelet/huh"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/sshexec"
"google.golang.org/protobuf/types/known/emptypb"
)
// TODO: support pinning the script version to the CLI version.
const installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
const (
// TODO: support pinning the script version to the CLI version.
installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
rootUser = "root"
)
type RemoteMachine struct {
User string
@@ -24,7 +32,7 @@ func installCmd(user string, version string) string {
var env []string
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
if user != "root" {
if user != rootUser {
sudoPrefix = "sudo"
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
}
@@ -46,6 +54,26 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
return fmt.Errorf("run whoami: %w", err)
}
if user != rootUser {
// 'sudo -n' is not used because it fails with 'sudo: a password is required' when the user has no password
// in /etc/shadow even though it may have valid sudo access.
out, err := exec.Run(ctx, "sudo true")
if err != nil {
if strings.Contains(out, "password is required") {
return fmt.Errorf(
"user '%[1]s' requires a password for sudo, but Uncloud needs passwordless sudo or root access "+
"to install and configure the uncloudd daemon on the remote machine.\n\n"+
"Possible solutions:\n"+
"1. Use root user or a user with passwordless sudo instead.\n"+
"2. Configure passwordless sudo for the user '%[1]s' by running on the remote machine:\n"+
" echo '%[1]s ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%[1]s",
user)
}
return fmt.Errorf("sudo command failed for user '%s': %w. "+
"Please ensure the user has sudo privileges or use root user instead", user, err)
}
}
cmd := installCmd(user, version)
fmt.Println("Downloading Uncloud install script:", installScriptURL)
@@ -56,3 +84,56 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
}
return nil
}
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error {
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"The remote machine is already initialised as a cluster member. Do you want to reset it first?\n" +
"This will:\n" +
"- Remove all service containers from the machine\n" +
"- Reset the machine to the uninitialised state",
).
Affirmative("Yes!").
Negative("No").
Value(&confirm),
),
).WithAccessible(true)
if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err)
}
if !confirm {
return fmt.Errorf("remote machine is already initialised as a cluster member")
}
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
"on the remote machine to fully uninstall Uncloud from it", err)
}
fmt.Println("Resetting the remote machine...")
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
}
return nil
}
// waitMachineReady waits for the machine to be ready to serve requests.
func waitMachineReady(ctx context.Context, machineClient pb.MachineClient, timeout time.Duration) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
inspect := func() error {
_, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
return nil
}
return backoff.Retry(inspect, boff)
}
+3 -2
View File
@@ -5,14 +5,15 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"golang.org/x/net/http2"
"log/slog"
"net"
"net/http"
"net/netip"
"net/url"
"time"
"github.com/cenkalti/backoff/v4"
"golang.org/x/net/http2"
)
const (
+2 -1
View File
@@ -6,11 +6,12 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"io"
"log/slog"
"net/http"
"strconv"
"github.com/cenkalti/backoff/v4"
)
type ChangeType string
+2 -1
View File
@@ -3,8 +3,9 @@ package daemon
import (
"context"
"fmt"
systemd "github.com/coreos/go-systemd/daemon"
"log/slog"
systemd "github.com/coreos/go-systemd/daemon"
"github.com/psviderski/uncloud/internal/machine"
)
+3 -2
View File
@@ -3,10 +3,11 @@ package daemon
import (
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/network"
"net/netip"
"os"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/network"
)
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
+3 -2
View File
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"log/slog"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
)
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
+350 -107
View File
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8, 0}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11, 0}
}
type AddMachineRequest struct {
@@ -339,6 +339,173 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
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 {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *RemoveMachineRequest) Reset() {
*x = RemoveMachineRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RemoveMachineRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveMachineRequest) ProtoMessage() {}
func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead.
func (*RemoveMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
}
func (x *RemoveMachineRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type Domain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -350,7 +517,7 @@ type Domain struct {
func (x *Domain) Reset() {
*x = Domain{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -363,7 +530,7 @@ func (x *Domain) String() string {
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -376,7 +543,7 @@ func (x *Domain) ProtoReflect() protoreflect.Message {
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *Domain) GetName() string {
@@ -397,7 +564,7 @@ type ReserveDomainRequest struct {
func (x *ReserveDomainRequest) Reset() {
*x = ReserveDomainRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -410,7 +577,7 @@ func (x *ReserveDomainRequest) String() string {
func (*ReserveDomainRequest) ProtoMessage() {}
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -423,7 +590,7 @@ func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
}
func (x *ReserveDomainRequest) GetEndpoint() string {
@@ -444,7 +611,7 @@ type CreateDomainRecordsRequest struct {
func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -457,7 +624,7 @@ func (x *CreateDomainRecordsRequest) String() string {
func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -470,7 +637,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
}
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
@@ -491,7 +658,7 @@ type CreateDomainRecordsResponse struct {
func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -504,7 +671,7 @@ func (x *CreateDomainRecordsResponse) String() string {
func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -517,7 +684,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{10}
}
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
@@ -540,7 +707,7 @@ type DNSRecord struct {
func (x *DNSRecord) Reset() {
*x = DNSRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -553,7 +720,7 @@ func (x *DNSRecord) String() string {
func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -566,7 +733,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11}
}
func (x *DNSRecord) GetName() string {
@@ -630,59 +797,87 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
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,
0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 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, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47,
0x0a, 0x1b, 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, 0x12, 0x28, 0x0a,
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07,
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e,
0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79,
0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f,
0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02,
0x32, 0x86, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a,
0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 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, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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,
0x69, 0x6e, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a,
0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x50, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01,
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f,
0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x70, 0x22, 0x43, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a,
0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x74, 0x22, 0x46, 0x0a, 0x1a, 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, 0x12,
0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 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,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x92, 0x04, 0x0a, 0x07,
0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
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, 0x19,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 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 (
@@ -698,7 +893,7 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
}
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
@@ -706,43 +901,54 @@ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
(*MachineMember)(nil), // 4: api.MachineMember
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
(*Domain)(nil), // 6: api.Domain
(*ReserveDomainRequest)(nil), // 7: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
(*DNSRecord)(nil), // 10: api.DNSRecord
(*NetworkConfig)(nil), // 11: api.NetworkConfig
(*IP)(nil), // 12: api.IP
(*MachineInfo)(nil), // 13: api.MachineInfo
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
(*UpdateMachineRequest)(nil), // 6: api.UpdateMachineRequest
(*UpdateMachineResponse)(nil), // 7: api.UpdateMachineResponse
(*RemoveMachineRequest)(nil), // 8: api.RemoveMachineRequest
(*Domain)(nil), // 9: api.Domain
(*ReserveDomainRequest)(nil), // 10: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 11: api.CreateDomainRecordsRequest
(*CreateDomainRecordsResponse)(nil), // 12: api.CreateDomainRecordsResponse
(*DNSRecord)(nil), // 13: api.DNSRecord
(*NetworkConfig)(nil), // 14: api.NetworkConfig
(*IP)(nil), // 15: api.IP
(*MachineInfo)(nil), // 16: api.MachineInfo
(*IPPort)(nil), // 17: api.IPPort
(*emptypb.Empty)(nil), // 18: google.protobuf.Empty
}
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
14, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
15, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
16, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
16, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
15, // [15:21] is the sub-list for method output_type
9, // [9:15] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
15, // 6: api.UpdateMachineRequest.public_ip:type_name -> api.IP
17, // 7: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
16, // 8: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
13, // 9: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
13, // 10: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 11: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
18, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
6, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
8, // 15: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
10, // 16: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
18, // 17: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
18, // 18: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
11, // 19: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 20: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 21: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
7, // 22: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
18, // 23: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
9, // 24: api.Cluster.ReserveDomain:output_type -> api.Domain
9, // 25: api.Cluster.GetDomain:output_type -> api.Domain
9, // 26: api.Cluster.ReleaseDomain:output_type -> api.Domain
12, // 27: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
20, // [20:28] is the sub-list for method output_type
12, // [12:20] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -802,7 +1008,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*Domain); i {
switch v := v.(*UpdateMachineRequest); i {
case 0:
return &v.state
case 1:
@@ -814,7 +1020,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); i {
switch v := v.(*UpdateMachineResponse); i {
case 0:
return &v.state
case 1:
@@ -826,7 +1032,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i {
switch v := v.(*RemoveMachineRequest); i {
case 0:
return &v.state
case 1:
@@ -838,7 +1044,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
switch v := v.(*Domain); i {
case 0:
return &v.state
case 1:
@@ -850,6 +1056,42 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); 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[9].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i {
case 0:
return &v.state
@@ -862,13 +1104,14 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 2,
NumMessages: 9,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
+20
View File
@@ -11,6 +11,8 @@ import "internal/machine/api/pb/machine.proto";
service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc GetDomain(google.protobuf.Empty) returns (Domain);
@@ -49,6 +51,24 @@ message ListMachinesResponse {
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 {
string id = 1;
}
message Domain {
string name = 1;
}
@@ -22,6 +22,8 @@ const _ = grpc.SupportPackageIsVersion9
const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_UpdateMachine_FullMethodName = "/api.Cluster/UpdateMachine"
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
@@ -34,6 +36,8 @@ const (
type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, 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)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
@@ -68,6 +72,26 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
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) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Cluster_RemoveMachine_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
@@ -114,6 +138,8 @@ func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomai
type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
@@ -134,6 +160,12 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
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) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveMachine not implemented")
}
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
}
@@ -203,6 +235,42 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
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) {
in := new(RemoveMachineRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).RemoveMachine(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_RemoveMachine_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).RemoveMachine(ctx, req.(*RemoveMachineRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReserveDomainRequest)
if err := dec(in); err != nil {
@@ -290,6 +358,14 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler,
},
{
MethodName: "UpdateMachine",
Handler: _Cluster_UpdateMachine_Handler,
},
{
MethodName: "RemoveMachine",
Handler: _Cluster_RemoveMachine_Handler,
},
{
MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler,
+114 -57
View File
@@ -466,6 +466,44 @@ func (x *TokenResponse) GetToken() string {
return ""
}
type ResetRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ResetRequest) Reset() {
*x = ResetRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResetRequest) ProtoMessage() {}
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
func (*ResetRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
}
type Service struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -480,7 +518,7 @@ type Service struct {
func (x *Service) Reset() {
*x = Service{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -493,7 +531,7 @@ func (x *Service) String() string {
func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -506,7 +544,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
}
func (x *Service) GetId() string {
@@ -548,7 +586,7 @@ type InspectServiceRequest struct {
func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -561,7 +599,7 @@ func (x *InspectServiceRequest) String() string {
func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -574,7 +612,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
}
func (x *InspectServiceRequest) GetId() string {
@@ -595,7 +633,7 @@ type InspectServiceResponse struct {
func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -608,7 +646,7 @@ func (x *InspectServiceResponse) String() string {
func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -621,7 +659,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
}
func (x *InspectServiceResponse) GetService() *Service {
@@ -644,7 +682,7 @@ type Service_Container struct {
func (x *Service_Container) Reset() {
*x = Service_Container{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -657,7 +695,7 @@ func (x *Service_Container) String() string {
func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -670,7 +708,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
func (*Service_Container) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
}
func (x *Service_Container) GetMachineId() string {
@@ -748,7 +786,8 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
@@ -767,7 +806,7 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
@@ -787,16 +826,19 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x32, 0x0a,
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -811,7 +853,7 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_machine_proto_rawDescData
}
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*MachineInfo)(nil), // 0: api.MachineInfo
(*NetworkConfig)(nil), // 1: api.NetworkConfig
@@ -820,42 +862,45 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
(*TokenResponse)(nil), // 6: api.TokenResponse
(*Service)(nil), // 7: api.Service
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
(*Service_Container)(nil), // 10: api.Service.Container
(*IP)(nil), // 11: api.IP
(*IPPrefix)(nil), // 12: api.IPPrefix
(*IPPort)(nil), // 13: api.IPPort
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
(*ResetRequest)(nil), // 7: api.ResetRequest
(*Service)(nil), // 8: api.Service
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
(*Service_Container)(nil), // 11: api.Service.Container
(*IP)(nil), // 12: api.IP
(*IPPrefix)(nil), // 13: api.IPPrefix
(*IPPort)(nil), // 14: api.IPPort
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
}
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
10, // 10: api.Service.containers:type_name -> api.Service.Container
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
11, // 10: api.Service.containers:type_name -> api.Service.Container
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
2, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
0, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
18, // [18:24] is the sub-list for method output_type
12, // [12:18] is the sub-list for method input_type
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
19, // [19:26] is the sub-list for method output_type
12, // [12:19] 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
@@ -953,7 +998,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*Service); i {
switch v := v.(*ResetRequest); i {
case 0:
return &v.state
case 1:
@@ -965,7 +1010,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i {
switch v := v.(*Service); i {
case 0:
return &v.state
case 1:
@@ -977,7 +1022,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i {
switch v := v.(*InspectServiceRequest); i {
case 0:
return &v.state
case 1:
@@ -989,6 +1034,18 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*Service_Container); i {
case 0:
return &v.state
@@ -1011,7 +1068,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0,
NumMessages: 11,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
+6
View File
@@ -14,6 +14,9 @@ service Machine {
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse);
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
}
@@ -61,6 +64,9 @@ message TokenResponse {
string token = 1;
}
message ResetRequest {
}
message Service {
string id = 1;
string name = 2;
@@ -25,6 +25,7 @@ const (
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_Reset_FullMethodName = "/api.Machine/Reset"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
)
@@ -38,6 +39,8 @@ type MachineClient interface {
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
}
@@ -99,6 +102,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
return out, nil
}
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Machine_Reset_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectServiceResponse)
@@ -119,6 +132,8 @@ type MachineServer interface {
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
mustEmbedUnimplementedMachineServer()
}
@@ -145,6 +160,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
}
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
}
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
}
@@ -259,6 +277,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler)
}
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).Reset(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_Reset_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).Reset(ctx, req.(*ResetRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectServiceRequest)
if err := dec(in); err != nil {
@@ -304,6 +340,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "Inspect",
Handler: _Machine_Inspect_Handler,
},
{
MethodName: "Reset",
Handler: _Machine_Reset_Handler,
},
{
MethodName: "InspectService",
Handler: _Machine_InspectService_Handler,
+2 -1
View File
@@ -2,10 +2,11 @@ package proxy
import (
"fmt"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
+2 -1
View File
@@ -2,11 +2,12 @@ package proxy
import (
"context"
"sync"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"sync"
)
// Director manages routing of gRPC requests between local and remote backends.
+2 -1
View File
@@ -2,11 +2,12 @@ package proxy
import (
"context"
"sync"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"sync"
)
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
+4 -3
View File
@@ -3,14 +3,15 @@ package proxy
import (
"context"
"fmt"
"net/netip"
"sync"
"time"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"net/netip"
"sync"
"time"
)
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
+2 -2
View File
@@ -29,7 +29,7 @@ type Controller struct {
func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
if err := os.MkdirAll(dir, 0o750); err != nil {
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
}
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
@@ -114,7 +114,7 @@ func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
if err = os.WriteFile(c.path, configBytes, 0o640); err != nil {
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
}
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
@@ -15,6 +15,7 @@ import (
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/docker"
@@ -25,11 +26,10 @@ import (
"google.golang.org/grpc"
)
const (
APIPort = 51000
)
type networkController struct {
// 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,
// and others.
type clusterController struct {
state *State
store *store.Store
@@ -39,25 +39,30 @@ type networkController struct {
server *grpc.Server
corroService corroservice.Service
dockerCli *client.Client
caddyfileCtrl *caddyconfig.Controller
dockerManager *docker.Manager
// dockerReady is signalled when Docker is configured and ready for containers.
dockerReady chan<- struct{}
caddyconfigCtrl *caddyconfig.Controller
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
dnsServer *dns.Server
dnsResolver *dns.ClusterResolver
// stopped is a channel that is closed when the controller is stopped.
stopped chan struct{}
}
func newNetworkController(
func newClusterController(
state *State,
store *store.Store,
server *grpc.Server,
corroService corroservice.Service,
dockerCli *client.Client,
dockerReady chan<- struct{},
caddyfileCtrl *caddyconfig.Controller,
dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver,
) (
*networkController, error,
) {
) (*clusterController, error) {
slog.Info("Starting WireGuard network.")
wgnet, err := network.NewWireGuardNetwork()
if err != nil {
@@ -65,7 +70,7 @@ func newNetworkController(
}
endpointChanges := wgnet.WatchEndpoints()
return &networkController{
return &clusterController{
state: state,
store: store,
wgnet: wgnet,
@@ -73,80 +78,90 @@ func newNetworkController(
server: server,
corroService: corroService,
dockerCli: dockerCli,
caddyfileCtrl: caddyfileCtrl,
dockerManager: docker.NewManager(dockerCli, state.ID, store),
dockerReady: dockerReady,
caddyconfigCtrl: caddyfileCtrl,
dnsServer: dnsServer,
dnsResolver: dnsResolver,
stopped: make(chan struct{}),
}, nil
}
func (nc *networkController) Run(ctx context.Context) error {
func (cc *clusterController) Run(ctx context.Context) error {
defer close(cc.stopped)
if err := firewall.ConfigureIptablesChains(); err != nil {
return fmt.Errorf("configure iptables chains: %w", err)
}
if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
if err := cc.ensureDockerNetwork(ctx); err != nil {
return err
}
slog.Info("Docker network configured.")
if err := cc.wgnet.Configure(*cc.state.Network); err != nil {
return fmt.Errorf("configure WireGuard network: %w", err)
}
slog.Info("WireGuard network configured.")
if nc.corroService.Running() {
if cc.corroService.Running() {
// Corrosion service was running before the WireGuard network was configured so we need to restart it.
slog.Info("Restarting corrosion service to apply new configuration with WireGuard network.")
if err := nc.corroService.Restart(ctx); err != nil {
if err := cc.corroService.Restart(ctx); err != nil {
return fmt.Errorf("restart corrosion service: %w", err)
}
slog.Info("Corrosion service restarted.")
} else {
slog.Info("Starting corrosion service.")
if err := nc.corroService.Start(ctx); err != nil {
if err := cc.corroService.Start(ctx); err != nil {
return fmt.Errorf("start corrosion service: %w", err)
}
slog.Info("Corrosion service started.")
}
// TODO: Figure out if we need to manually stop the corrosion service when the context is done or just
// rely on systemd to handle service dependencies on its own.
errGroup, ctx := errgroup.WithContext(ctx)
// Start the network API server. Assume the management IP can't be changed when the network is running.
apiAddr := net.JoinHostPort(nc.state.Network.ManagementIP.String(), strconv.Itoa(APIPort))
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
listener, err := net.Listen("tcp", apiAddr)
if err != nil {
return fmt.Errorf("listen API port: %w", err)
}
errGroup.Go(
func() error {
slog.Info("Starting network API server.", "addr", apiAddr)
if err := nc.server.Serve(listener); err != nil {
return fmt.Errorf("network API server failed: %w", err)
}
return nil
},
)
errGroup.Go(func() error {
slog.Info("Starting network API server.", "addr", apiAddr)
if err := cc.server.Serve(listener); err != nil {
return fmt.Errorf("network API server failed: %w", err)
}
return nil
})
errGroup.Go(func() error {
slog.Info("Starting embedded DNS resolver.")
if err := nc.dnsResolver.Run(ctx); err != nil {
if err := cc.dnsResolver.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS resolver failed: %w", err)
}
return nil
})
// The Docker network must be created before starting the DNS server because it listens on the machine IP.
errGroup.Go(func() error {
slog.Info("Starting embedded DNS server.")
if err := nc.dnsServer.Run(ctx); err != nil {
if err := cc.dnsServer.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS server failed: %w", err)
}
return nil
})
// Setup Docker network and synchronise containers to the cluster store.
// Synchronise Docker containers to the cluster store.
errGroup.Go(func() error {
return nc.prepareAndWatchDocker(ctx)
slog.Info("Watching Docker containers and syncing them to cluster store.")
return cc.syncDockerContainers(ctx)
})
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
// in separate goroutines to avoid a deadlock when reconfiguring the network.
errGroup.Go(func() error {
if err := nc.handleMachineChanges(ctx); err != nil {
if err := cc.handleMachineChanges(ctx); err != nil {
return fmt.Errorf("handle new machines: %w", err)
}
return nil
@@ -156,24 +171,24 @@ func (nc *networkController) Run(ctx context.Context) error {
errGroup.Go(func() error {
for {
select {
case e, ok := <-nc.endpointChanges:
case e, ok := <-cc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
nc.endpointChanges = nil
cc.endpointChanges = nil
return nil
}
nc.state.mu.Lock()
for i := range nc.state.Network.Peers {
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
cc.state.mu.Lock()
for i := range cc.state.Network.Peers {
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := nc.state.Save(); err != nil {
if err := cc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
nc.state.mu.Unlock()
cc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
@@ -184,48 +199,65 @@ func (nc *networkController) Run(ctx context.Context) error {
})
errGroup.Go(func() error {
if err := nc.wgnet.Run(ctx); err != nil {
if err := cc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
})
errGroup.Go(func() error {
slog.Info("Starting Caddyconfig controller.")
if err := nc.caddyfileCtrl.Run(ctx); err != nil {
//goland:noinspection GoErrorStringFormat
return fmt.Errorf("Caddyconfig controller failed: %w", err)
slog.Info("Starting caddyconfig controller.")
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
return fmt.Errorf("caddyconfig controller failed: %w", err)
}
return nil
})
// Wait for the context to be done and stop the network API server.
errGroup.Go(func() error {
<-ctx.Done()
slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown.
nc.server.GracefulStop()
slog.Info("Network API server stopped.")
return nil
})
<-ctx.Done()
slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown.
cc.server.GracefulStop()
slog.Info("Network API server stopped.")
return errGroup.Wait()
// Wait for all controllers to finish.
err = errGroup.Wait()
// It's safe to stop the Corrosion service after the controllers depending on it and API server are stopped.
// Use a new context with a timeout as the current context is already canceled.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if corroErr := cc.corroService.Stop(ctx); corroErr != nil {
err = errors.Join(err, fmt.Errorf("stop corrosion service: %w", corroErr))
} else {
slog.Info("Corrosion service stopped.")
}
return err
}
// prepareAndWatchDocker configures the Docker network and watches local Docker containers to sync them
// to the cluster store.
func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
manager := docker.NewManager(nc.dockerCli, nc.state.ID, nc.store)
if err := manager.WaitDaemonReady(ctx); err != nil {
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
if err := cc.dockerManager.WaitDaemonReady(ctx); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet, nc.dnsServer.ListenAddr()); err != nil {
if err := cc.dockerManager.EnsureUncloudNetwork(
ctx,
cc.state.Network.Subnet,
cc.dnsServer.ListenAddr(),
); err != nil {
return fmt.Errorf("ensure Docker network: %w", err)
}
slog.Info("Docker network configured.")
slog.Info("Watching Docker containers and syncing them to cluster store.")
// Signal that Docker is ready for containers.
close(cc.dockerReady)
return nil
}
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
// Retry to watch and sync containers until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
@@ -233,7 +265,7 @@ func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
backoff.WithMaxElapsedTime(0),
), ctx)
watchAndSync := func() error {
if wErr := manager.WatchAndSyncContainers(ctx); wErr != nil {
if wErr := cc.dockerManager.WatchAndSyncContainers(ctx); wErr != nil {
slog.Error("Failed to watch and sync containers to cluster store, retrying.", "err", wErr)
return wErr
}
@@ -251,7 +283,7 @@ func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly
// when changes occur.
func (nc *networkController) handleMachineChanges(ctx context.Context) error {
func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
for {
// Retry to subscribe to machine changes indefinitely until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
@@ -266,7 +298,7 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
err error
)
subscribe := func() error {
if machines, changes, err = nc.store.SubscribeMachines(ctx); err != nil {
if machines, changes, err = cc.store.SubscribeMachines(ctx); err != nil {
slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
}
return err
@@ -284,7 +316,7 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
// completes. Skip configuration now and apply it when the store changes are received.
if len(machines) > 0 {
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
if err = nc.configurePeers(machines); err != nil {
if err = cc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
}
@@ -296,11 +328,11 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
// be reworked as well.
case <-changes:
slog.Info("Cluster machines changed, reconfiguring network peers.")
if machines, err = nc.store.ListMachines(ctx); err != nil {
if machines, err = cc.store.ListMachines(ctx); err != nil {
slog.Error("Failed to list machines.", "err", err)
continue
}
if err = nc.configurePeers(machines); err != nil {
if err = cc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
case <-ctx.Done():
@@ -310,23 +342,23 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
}
}
func (nc *networkController) configurePeers(machines []*pb.MachineInfo) error {
func (cc *clusterController) configurePeers(machines []*pb.MachineInfo) error {
if len(machines) == 0 {
return fmt.Errorf("no machines to configure peers")
}
nc.state.mu.RLock()
currentPeerEndpoints := make(map[string]*netip.AddrPort, len(nc.state.Network.Peers))
for _, p := range nc.state.Network.Peers {
cc.state.mu.RLock()
currentPeerEndpoints := make(map[string]*netip.AddrPort, len(cc.state.Network.Peers))
for _, p := range cc.state.Network.Peers {
currentPeerEndpoints[p.PublicKey.String()] = p.Endpoint
}
nc.state.mu.RUnlock()
cc.state.mu.RUnlock()
// Construct the list of peers from the machine configurations ensuring that the current endpoint is preserved.
peers := make([]network.PeerConfig, 0, len(machines)-1)
for _, m := range machines {
// Skip the current machine.
if m.Id == nc.state.ID {
if m.Id == cc.state.ID {
continue
}
if err := m.Network.Validate(); err != nil {
@@ -359,20 +391,37 @@ func (nc *networkController) configurePeers(machines []*pb.MachineInfo) error {
}
// Preserve the new list of peers in the machine state.
nc.state.mu.Lock()
nc.state.Network.Peers = peers
err := nc.state.Save()
nc.state.mu.Unlock()
cc.state.mu.Lock()
cc.state.Network.Peers = peers
err := cc.state.Save()
cc.state.mu.Unlock()
if err != nil {
return fmt.Errorf("save machine state: %w", err)
}
nc.state.mu.RLock()
defer nc.state.mu.RUnlock()
if err = nc.wgnet.Configure(*nc.state.Network); err != nil {
cc.state.mu.RLock()
defer cc.state.mu.RUnlock()
if err = cc.wgnet.Configure(*cc.state.Network); err != nil {
return fmt.Errorf("configure network peers: %w", err)
}
return nil
}
// TODO: method to shutdown network when leaving a cluster. Regular context cancellation shouldn't bring it down.
// Cleanup cleans up the cluster resources such as the WireGuard network, iptables rules, Docker network and containers.
func (cc *clusterController) Cleanup() error {
// Wait for the controller to stop before cleaning up.
<-cc.stopped
var errs []error
if err := cc.dockerManager.Cleanup(); err != nil {
errs = append(errs, fmt.Errorf("cleanup Docker resources: %w", err))
}
if err := cc.wgnet.Cleanup(); err != nil {
errs = append(errs, fmt.Errorf("cleanup WireGuard network: %w", err))
}
if err := firewall.CleanupIptablesChains(); err != nil {
errs = append(errs, fmt.Errorf("cleanup iptables chains: %w", err))
}
return errors.Join(errs...)
}
+108 -3
View File
@@ -5,17 +5,18 @@ import (
"context"
"errors"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"log/slog"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/internal/secret"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type Cluster struct {
@@ -197,6 +198,89 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
return resp, nil
}
// UpdateMachine updates machine configuration in the cluster.
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
if err := c.checkInitialised(ctx); 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 req.Endpoints != nil {
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.
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
if err := c.checkInitialised(ctx); err != nil {
@@ -242,3 +326,24 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
return &pb.ListMachinesResponse{Machines: members}, nil
}
// RemoveMachine removes a machine from the cluster.
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "machine ID not set")
}
if err := c.store.DeleteMachine(ctx, req.Id); err != nil {
if errors.Is(err, store.ErrMachineNotFound) {
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.Id)
}
return nil, status.Errorf(codes.Internal, "delete machine from store: %v", err)
}
slog.Info("Machine removed from the cluster.", "id", req.Id)
return &emptypb.Empty{}, nil
}
+1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
+2 -1
View File
@@ -3,8 +3,9 @@ package cluster
import (
"errors"
"fmt"
"go4.org/netipx"
"net/netip"
"go4.org/netipx"
)
const DefaultSubnetBits = 24
+1
View File
@@ -2,6 +2,7 @@ package cluster
import (
"fmt"
"github.com/psviderski/uncloud/internal/secret"
)
+6
View File
@@ -0,0 +1,6 @@
package constants
const (
// MachineAPIPort is the port for the Machine API service on the management WireGuard network.
MachineAPIPort = 51000
)
+5 -4
View File
@@ -3,10 +3,11 @@ package corroservice
import (
"bytes"
"fmt"
"github.com/BurntSushi/toml"
"net/netip"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/psviderski/uncloud/internal/fs"
)
@@ -50,7 +51,7 @@ func (c *Config) Write(path, owner string) error {
if err := encoder.Encode(c); err != nil {
return fmt.Errorf("encode config: %w", err)
}
if err := os.WriteFile(path, data.Bytes(), 0600); err != nil {
if err := os.WriteFile(path, data.Bytes(), 0o600); err != nil {
return err
}
if err := fs.Chown(path, owner, owner); err != nil {
@@ -62,10 +63,10 @@ func (c *Config) Write(path, owner string) error {
func MkDataDir(dir, owner string) error {
parent, _ := filepath.Split(dir)
// Use 0711 for parent directories to allow `owner` to access its nested data directory.
if err := os.MkdirAll(parent, 0711); err != nil {
if err := os.MkdirAll(parent, 0o711); err != nil {
return fmt.Errorf("create directory %q: %w", parent, err)
}
if err := os.Mkdir(dir, 0700); err != nil {
if err := os.Mkdir(dir, 0o700); err != nil {
if !os.IsExist(err) {
return fmt.Errorf("create directory %q: %w", dir, err)
}
+40 -21
View File
@@ -3,15 +3,16 @@ package corroservice
import (
"context"
"fmt"
"io"
"log/slog"
"path/filepath"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"io"
"log/slog"
"path/filepath"
"time"
)
const (
@@ -26,29 +27,47 @@ type DockerService struct {
User string
}
func NewDockerService(cli *client.Client, image, name, dataDir string) *DockerService {
return &DockerService{
Client: cli,
Image: image,
Name: name,
DataDir: dataDir,
}
}
func (s *DockerService) Start(ctx context.Context) error {
_, err := s.Client.ContainerInspect(ctx, s.Name)
if err != nil {
if client.IsErrNotFound(err) {
return s.startNewContainer(ctx)
if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect container %q: %w", s.Name, err)
}
if err = s.startNewContainer(ctx); err != nil {
return err
}
} else {
// Container already exists.
// TODO: recreate only if the container configuration has to be changed.
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
return fmt.Errorf("remove container %q: %w", s.Name, err)
}
if err = s.startNewContainer(ctx); err != nil {
return err
}
return fmt.Errorf("inspect container %q: %w", s.Name, err)
}
// TODO: recreate only if the container configuration has to be changed.
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
return fmt.Errorf("remove container %q: %w", s.Name, err)
}
return s.startNewContainer(ctx)
slog.Debug("Waiting for corrosion service to be ready.")
if err = WaitReady(ctx, s.DataDir); err != nil {
return err
}
slog.Debug("Corrosion service is ready.")
return nil
}
func (s *DockerService) Stop(ctx context.Context) error {
if err := s.Client.ContainerStop(ctx, s.Name, container.StopOptions{}); err != nil {
return fmt.Errorf("stop container %q: %w", s.Name, err)
}
slog.Debug("Corrosion Docker container stopped.", "name", s.Name)
if err := s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{}); err != nil {
return fmt.Errorf("remove container %q: %w", s.Name, err)
}
slog.Debug("Corrosion Docker container removed.", "name", s.Name)
return nil
}
func (s *DockerService) Restart(ctx context.Context) error {
+54 -1
View File
@@ -1,9 +1,62 @@
package corroservice
import "context"
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/BurntSushi/toml"
"github.com/cenkalti/backoff/v4"
"github.com/psviderski/uncloud/internal/corrosion"
)
type Service interface {
Start(ctx context.Context) error
Stop(ctx context.Context) error
Restart(ctx context.Context) error
Running() bool
}
// WaitReady waits for the Corrosion service to be ready with the uncloud schema applied.
func WaitReady(ctx context.Context, dataDir string) error {
// Read the config file to get the API address.
configPath := filepath.Join(dataDir, "config.toml")
configData, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("read config file: %w", err)
}
var config Config
if err = toml.Unmarshal(configData, &config); err != nil {
return fmt.Errorf("unmarshal config: %w", err)
}
corro, err := corrosion.NewAPIClient(config.API.Addr)
if err != nil {
return fmt.Errorf("create corrosion API client: %w", err)
}
// Corrosion starts serving the API before applying the schema. Query the cluster table with exponential backoff
// to check if the uncloud schema has been applied.
checkReady := func() error {
rows, err := corro.QueryContext(ctx, "SELECT 1 FROM cluster LIMIT 1")
if err != nil {
return fmt.Errorf("query cluster table: %w", err)
}
defer rows.Close()
return nil
}
b := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(50*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(15*time.Second),
)
if err = backoff.Retry(checkReady, backoff.WithContext(b, ctx)); err != nil {
return fmt.Errorf("corrosion service did not become ready: %w", err)
}
return nil
}
-157
View File
@@ -1,157 +0,0 @@
package corroservice
import (
"bufio"
"context"
"fmt"
"log/slog"
"os/exec"
"path/filepath"
"sync"
"syscall"
"time"
)
const (
DefaultCommand = "corrosion"
DefaultDataDir = "/var/lib/uncloud/corrosion"
)
// SubprocessService implements the Service interface by running the service as a subprocess.
type SubprocessService struct {
Command string
DataDir string
cmd *exec.Cmd
running bool
mu sync.Mutex
cancelWatch context.CancelFunc
}
func DefaultSubprocessService() *SubprocessService {
return &SubprocessService{
Command: DefaultCommand,
DataDir: DefaultDataDir,
}
}
// TODO: maybe stop the process if this ctx is cancelled.
func (s *SubprocessService) Start(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.running {
return nil
}
return s.startProcess(ctx)
}
func (s *SubprocessService) Restart(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.running {
if err := s.stopProcess(); err != nil {
return fmt.Errorf("stop process: %w", err)
}
}
return s.startProcess(ctx)
}
func (s *SubprocessService) Running() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.running
}
func (s *SubprocessService) startProcess(ctx context.Context) error {
s.cmd = exec.Command(s.Command, "agent", "-c", filepath.Join(s.DataDir, "config.toml"))
// Redirect stdout and stderr to the logger.
stdout, err := s.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create stdout pipe: %w", err)
}
stderr, err := s.cmd.StderrPipe()
if err != nil {
return fmt.Errorf("create stderr pipe: %w", err)
}
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
slog.Info("[corrosion]: " + scanner.Text())
}
// TODO: remove
slog.Info("######## corrosion redirect go routine end ########")
}()
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
slog.Error("[corrosion]: " + scanner.Text())
}
}()
if err = s.cmd.Start(); err != nil {
return fmt.Errorf("start process: %w", err)
}
s.running = true
// Watch for process exit to update running status.
go func() {
if err := s.cmd.Wait(); err != nil {
slog.Error("corrosion process exited with error.", "code", s.cmd.ProcessState.ExitCode(), "err", err)
}
s.mu.Lock()
s.running = false
s.mu.Unlock()
}()
// TODO: figure out the waiting process
// Wait for initialization
//timer := time.NewTimer(2 * time.Second)
//defer timer.Stop()
//select {
////case <-timer.C:
//// s.running = true
//// return nil
//case <-watchCtx.Done():
// return fmt.Errorf("process failed to start")
//case <-ctx.Done():
// s.stopProcess()
// return ctx.Err()
//}
return nil
}
func (s *SubprocessService) stopProcess() error {
if s.cmd == nil || s.cmd.Process == nil {
return nil
}
if err := s.cmd.Process.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("send SIGTERM: %w", err)
}
// Wait up to 5 seconds for graceful shutdown before killing the process.
done := make(chan error, 1)
go func() {
done <- s.cmd.Wait()
}()
select {
case <-time.After(5 * time.Second):
if err := s.cmd.Process.Kill(); err != nil {
return fmt.Errorf("kill process: %w", err)
}
case err := <-done:
if err != nil {
return fmt.Errorf("process exited with error: %w", err)
}
}
return nil
}
+15 -12
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"os/exec"
"time"
)
const DefaultSystemdUnit = "uncloud-corrosion.service"
@@ -27,6 +26,15 @@ func (s *SystemdService) Start(ctx context.Context) error {
return s.startOrRestart(ctx, "start")
}
func (s *SystemdService) Stop(_ context.Context) error {
if _, err := exec.Command("systemctl", "stop", s.Unit).Output(); err != nil {
return fmt.Errorf("systemctl stop %s: %w", s.Unit, err)
}
slog.Info("Corrosion systemd service stopped.", "unit", s.Unit)
return nil
}
func (s *SystemdService) Restart(ctx context.Context) error {
return s.startOrRestart(ctx, "restart")
}
@@ -35,20 +43,15 @@ func (s *SystemdService) startOrRestart(ctx context.Context, cmd string) error {
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
}
slog.Info(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
slog.Debug(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
// Optimistically wait for the corrosion service to start and initialise the database schema before proceeding.
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
case <-ctx.Done():
return nil
slog.Debug("Waiting for corrosion service to be ready.")
if err := WaitReady(ctx, s.DataDir); err != nil {
return err
}
// TODO: run a goroutine to check the status of the service and log any errors in the uncloud log.
slog.Debug("Corrosion service is ready.")
s.running = true
return nil
}
+2 -2
View File
@@ -14,13 +14,13 @@ const DBFileName = "machine.db"
func NewDB(path string) (*sqlx.DB, error) {
// Create the database file with 0600 permissions if it doesn't exist, or update permissions if exists.
if _, err := os.Stat(path); os.IsNotExist(err) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
}
file.Close()
} else {
if err = os.Chmod(path, 0600); err != nil {
if err = os.Chmod(path, 0o600); err != nil {
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
}
}
+1
View File
@@ -155,6 +155,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelServiceName),
filters.Arg("label", api.LabelManaged),
),
})
if err != nil {
@@ -12,3 +12,8 @@ import (
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
return fmt.Errorf("not supported on Darwin")
}
// Cleanup is a stub for Darwin.
func (m *Manager) Cleanup() error {
return fmt.Errorf("not supported on Darwin")
}
+130 -3
View File
@@ -2,17 +2,21 @@ package docker
import (
"context"
"errors"
"fmt"
"log/slog"
"net/netip"
"strconv"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/libnetwork/iptables"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api"
)
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
@@ -52,6 +56,14 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix,
},
},
},
Labels: map[string]string{
api.LabelManaged: "",
},
Options: map[string]string{
// Starting with Docker 28.2.0 (https://github.com/moby/moby/pull/49832), we have to explicitly
// allow direct routing from the WireGuard interface to the bridge network.
"com.docker.network.bridge.trusted_host_interfaces": network.WireGuardInterfaceName,
},
},
); err != nil {
return fmt.Errorf("create Docker network '%s': %w", NetworkName, err)
@@ -72,7 +84,7 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix,
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
bridgeName := "br-" + nw.ID[:12]
if err = configureIptables(bridgeName, dnsServer); err != nil {
if err = configureIptables(bridgeName, subnet, dnsServer); err != nil {
return fmt.Errorf("configure iptables for Docker network '%s': %w", NetworkName, err)
}
@@ -80,11 +92,15 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix,
}
// configureIptables configures iptables rules for the uncloud Docker network.
func configureIptables(bridgeName string, dnsServer netip.Addr) error {
func configureIptables(bridgeName string, subnet netip.Prefix, dnsServer netip.Addr) error {
ipt := iptables.GetIptable(iptables.IPv4)
// Allow traffic from other machines and their containers through the WG mesh to the Uncloud containers
// on the machine.
wgRule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
wgRule := []string{
"--in-interface", network.WireGuardInterfaceName,
"--out-interface", bridgeName,
"-j", "ACCEPT",
}
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Insert, wgRule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
@@ -103,5 +119,116 @@ func configureIptables(bridgeName string, dnsServer netip.Addr) error {
}
}
// Skip masquerading for the container traffic going from the uncloud Docker network through the WG mesh.
// https://uncloud.run/blog/connect-docker-containers-across-hosts-wireguard#step-3-configure-ip-routing
skipMasqueradeRule := []string{
"--src", subnet.String(),
"--out-interface", network.WireGuardInterfaceName,
"-j", "RETURN",
}
// Delete and reinsert the rule to ensure it's at the top of the POSTROUTING chain before the MASQUERADE rule
// added by Docker: POSTROUTING -s 10.210.X.0/24 ! -o br-XXX -j MASQUERADE
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Delete, skipMasqueradeRule); err != nil {
return fmt.Errorf("delete iptables rule: %w", err)
}
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Insert, skipMasqueradeRule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
return nil
}
// cleanupIptables deletes the iptables rules for the uncloud Docker network.
func cleanupIptables(bridgeName string, subnet netip.Prefix) error {
ipt := iptables.GetIptable(iptables.IPv4)
// Delete the rule allowing traffic from the WireGuard network to the Docker bridge.
wgRule := []string{
"--in-interface", network.WireGuardInterfaceName,
"--out-interface", bridgeName,
"-j", "ACCEPT",
}
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Delete, wgRule); err != nil {
return fmt.Errorf("delete iptables rule: %w", err)
}
// Delete the rule that skips masquerading for the container traffic going from the uncloud Docker network
// through the WG mesh.
skipMasqueradeRule := []string{
"--src", subnet.String(),
"--out-interface", network.WireGuardInterfaceName,
"-j", "RETURN",
}
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Delete, skipMasqueradeRule); err != nil {
return fmt.Errorf("delete iptables rule: %w", err)
}
// Rules in uncloud-owned chains will be automatically cleaned up by the machine cleanup.
return nil
}
// Cleanup removes all uncloud-managed containers and the uncloud Docker network.
func (m *Manager) Cleanup() error {
ctx := context.Background()
var errs []error
// Remove uncloud-managed Docker containers.
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
All: true, // Include stopped containers.
Filters: filters.NewArgs(
filters.Arg("label", api.LabelManaged),
),
})
if err != nil {
errs = append(errs, fmt.Errorf("list uncloud-managed Docker containers: %w", err))
} else if len(containers) > 0 {
slog.Info("Removing uncloud-managed Docker containers.", "count", len(containers))
removed := 0
for _, ctr := range containers {
err = m.client.ContainerStop(ctx, ctr.ID, dockercontainer.StopOptions{})
if err != nil && !client.IsErrNotFound(err) {
errs = append(errs, fmt.Errorf("stop container '%s': %w", ctr.ID, err))
}
err = m.client.ContainerRemove(ctx, ctr.ID, dockercontainer.RemoveOptions{
// Remove anonymous volumes created by the container.
RemoveVolumes: true,
})
if err == nil {
removed++
} else if !client.IsErrNotFound(err) {
errs = append(errs, fmt.Errorf("remove container '%s': %w", ctr.ID, err))
}
}
slog.Info("Removed uncloud-managed Docker containers.", "count", removed)
}
// Remove the uncloud Docker network and related iptables rules.
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err == nil {
bridgeName := "br-" + nw.ID[:12]
var subnet netip.Prefix
if len(nw.IPAM.Config) > 0 {
subnet, _ = netip.ParsePrefix(nw.IPAM.Config[0].Subnet)
}
if subnet.IsValid() {
if err = cleanupIptables(bridgeName, subnet); err != nil {
errs = append(errs, fmt.Errorf("cleanup iptables for Docker network '%s': %w", NetworkName, err))
} else {
slog.Info("Cleaned up iptables rules for Docker network.", "name", NetworkName, "bridge", bridgeName)
}
}
if err = m.client.NetworkRemove(ctx, NetworkName); err == nil {
slog.Info("Docker network removed.", "name", NetworkName)
} else if !client.IsErrNotFound(err) {
errs = append(errs, fmt.Errorf("remove Docker network '%s': %w", NetworkName, err))
}
} else if !client.IsErrNotFound(err) {
errs = append(errs, fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err))
}
return errors.Join(errs...)
}
+40 -4
View File
@@ -50,15 +50,42 @@ type Server struct {
// internalDNSIP is a function that returns the IP address of the internal DNS server. It may return an empty
// address if the address is unknown (e.g. when the machine is not initialised yet).
internalDNSIP func() netip.Addr
// networkReady is a function that returns true if the Docker network is ready for containers.
networkReady func() bool
// waitForNetworkReady is a function that waits for the Docker network to be ready for containers.
waitForNetworkReady func(ctx context.Context) error
}
// ServerOption configures the Docker server.
type ServerOption func(*Server)
// WithNetworkReady sets the network readiness check function.
func WithNetworkReady(networkReady func() bool) ServerOption {
return func(s *Server) {
s.networkReady = networkReady
}
}
// WithWaitForNetworkReady sets the network readiness wait function.
func WithWaitForNetworkReady(waitForNetworkReady func(ctx context.Context) error) ServerOption {
return func(s *Server) {
s.waitForNetworkReady = waitForNetworkReady
}
}
// NewServer creates a new Docker gRPC server with the provided Docker client.
func NewServer(cli *client.Client, db *sqlx.DB, internalDNSIP func() netip.Addr) *Server {
return &Server{
func NewServer(cli *client.Client, db *sqlx.DB, internalDNSIP func() netip.Addr, opts ...ServerOption) *Server {
s := &Server{
client: cli,
db: db,
internalDNSIP: internalDNSIP,
}
for _, opt := range opts {
opt(s)
}
return s
}
// CreateContainer creates a new container based on the given configuration.
@@ -118,6 +145,15 @@ func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerR
// StartContainer starts a container with the given ID and options.
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
// Wait for Docker network to be ready before starting the container
if s.waitForNetworkReady != nil {
if err := s.waitForNetworkReady(ctx); err != nil {
return nil, status.Errorf(codes.Unavailable, "Docker network not ready: %v", err)
}
} else if s.networkReady != nil && !s.networkReady() {
return nil, status.Errorf(codes.Unavailable, "Docker network not ready")
}
var opts container.StartOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
@@ -510,10 +546,10 @@ func (s *Server) CreateServiceContainer(
Memory: spec.Container.Resources.Memory,
MemoryReservation: spec.Container.Resources.MemoryReservation,
},
// Always restart service containers if they exit or a machine restarts.
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
// For one-off containers and batch jobs we plan to use a different service type/mode.
RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways,
Name: container.RestartPolicyUnlessStopped,
},
}
@@ -6,3 +6,8 @@ import "fmt"
func ConfigureIptablesChains() error {
return fmt.Errorf("not supported on Darwin")
}
// CleanupIptablesChains is a stub for Darwin.
func CleanupIptablesChains() error {
return fmt.Errorf("not supported on Darwin")
}
+134 -45
View File
@@ -2,10 +2,13 @@ package firewall
import (
"fmt"
"log/slog"
"strconv"
"strings"
"github.com/docker/docker/libnetwork/iptables"
"github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/network"
)
@@ -16,59 +19,145 @@ const (
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
func ConfigureIptablesChains() error {
// Ensure iptables UNCLOUD-INPUT chain with a RETURN rule exists. All existing rules are flushed.
ipt := iptables.GetIptable(iptables.IPv4)
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
return fmt.Errorf("create iptables chain '%s': %w", UncloudInputChain, err)
}
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
}
if err := ipt.AddReturnRule(UncloudInputChain); err != nil {
return fmt.Errorf("add the RETURN rule for iptables chain '%s': %w", UncloudInputChain, err)
if err := createIptablesChains(); err != nil {
return err
}
// Ensure the main iptables INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
// Look for the first DROP/REJECT rule in the INPUT chain.
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
if err != nil {
return fmt.Errorf("get iptables rules for chain '%s': %w", UncloudInputChain, err)
}
firstRejectRuleNum := 0
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
if fields[1] == "DROP" || fields[1] == "REJECT" {
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
firstRejectRuleNum = ruleNum
break
}
}
}
var addJumpRule []string
if firstRejectRuleNum > 0 {
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT", strconv.Itoa(firstRejectRuleNum)},
jumpRule...)
} else {
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
}
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
return fmt.Errorf("add iptables rule '%s': %w", strings.Join(addJumpRule, " "), err)
}
}
ipt4 := iptables.GetIptable(iptables.IPv4)
ipt6 := iptables.GetIptable(iptables.IPv6)
// Allow WireGuard traffic to the machine.
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
err := ipt.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
err := ipt4.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
if err != nil {
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
}
// Allow cluster machines to access Machine API via the management IPv6 WireGuard network.
acceptMachineAPIRule := []string{
"-i", network.WireGuardInterfaceName,
"-s", "fdcc::/16",
"-p", "tcp",
"--dport", strconv.Itoa(constants.MachineAPIPort),
"-j", "ACCEPT",
}
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
acceptCorrosionGossipRule := []string{
"-i", network.WireGuardInterfaceName,
"-s", "fdcc::/16",
"-p", "udp",
"--dport", strconv.Itoa(corroservice.DefaultGossipPort),
"-j", "ACCEPT",
}
for _, rule := range [][]string{acceptMachineAPIRule, acceptCorrosionGossipRule} {
if err = ipt6.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, rule); err != nil {
return fmt.Errorf("insert ip6tables rule '%s': %w", strings.Join(rule, " "), err)
}
}
return nil
}
// createIptablesChains ensures UNCLOUD-INPUT iptables and ip6tables chains exist and
// there are jump rules from the main INPUT chains.
func createIptablesChains() error {
ipt4 := iptables.GetIptable(iptables.IPv4)
ipt6 := iptables.GetIptable(iptables.IPv6)
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
iptBin := "iptables"
if i == 1 {
iptBin = "ip6tables"
}
// Ensure UNCLOUD-INPUT chain exists. All existing rules are flushed.
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
return fmt.Errorf("create %s chain '%s': %w", iptBin, UncloudInputChain, err)
}
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
}
// Ensure the main INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
// Look for the first DROP/REJECT rule in the INPUT chain.
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
if err != nil {
return fmt.Errorf("get %s rules for chain '%s': %w", iptBin, UncloudInputChain, err)
}
firstRejectRuleNum := 0
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
if fields[1] == "DROP" || fields[1] == "REJECT" {
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
firstRejectRuleNum = ruleNum
break
}
}
}
var addJumpRule []string
if firstRejectRuleNum > 0 {
addJumpRule = append([]string{
"-t", string(iptables.Filter),
"-I", "INPUT",
strconv.Itoa(firstRejectRuleNum),
}, jumpRule...)
} else {
addJumpRule = append([]string{
"-t", string(iptables.Filter),
"-A", "INPUT",
}, jumpRule...)
}
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
return fmt.Errorf("add %s rule '%s': %w", iptBin, strings.Join(addJumpRule, " "), err)
}
}
}
return nil
}
// CleanupIptablesChains removes the custom iptables chains and rules created by ConfigureIptablesChains.
func CleanupIptablesChains() error {
ipt4 := iptables.GetIptable(iptables.IPv4)
ipt6 := iptables.GetIptable(iptables.IPv6)
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
iptBin := "iptables"
if i == 1 {
iptBin = "ip6tables"
}
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
return fmt.Errorf("delete %s jump rule from INPUT: %w", iptBin, err)
}
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
// Chain might not exist which is fine.
if !strings.Contains(err.Error(), "No chain") {
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
}
}
// Delete the UNCLOUD-INPUT chain.
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
// Chain might not exist which is fine.
if !strings.Contains(err.Error(), "No chain") {
return fmt.Errorf("delete %s chain '%s': %w", iptBin, UncloudInputChain, err)
}
} else {
slog.Info(fmt.Sprintf("Deleted %s chain.", iptBin), "chain", UncloudInputChain)
}
}
return nil
}
+212 -130
View File
@@ -13,6 +13,7 @@ import (
"path/filepath"
"slices"
"strconv"
"sync"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
@@ -23,6 +24,7 @@ import (
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
@@ -149,7 +151,14 @@ type Machine struct {
started chan struct{}
// initialised is signalled when the machine is configured as a member of a cluster.
initialised chan struct{}
// networkReady is signalled when the Docker network is configured and ready for containers.
networkReady chan struct{}
// resetting is true when the machine is being reset.
resetting bool
// stop cancels the Run method context to stop the machine gracefully.
stop func()
clusterCtrl *clusterController
// store is the cluster store backed by a distributed Corrosion database.
store *store.Store
cluster *cluster.Cluster
@@ -163,6 +172,9 @@ type Machine struct {
// It proxies requests to the local or remote machine API servers depending on the request targets
// and aggregates responses.
localProxyServer *grpc.Server
// mu protects the Machine from concurrent reads and writes.
mu sync.RWMutex
}
func NewMachine(config *Config) (*Machine, error) {
@@ -222,7 +234,7 @@ func NewMachine(config *Config) (*Machine, error) {
}
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort)
localProxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnknownServiceHandler(
@@ -235,6 +247,7 @@ func NewMachine(config *Config) (*Machine, error) {
state: state,
started: make(chan struct{}),
initialised: make(chan struct{}, 1),
networkReady: make(chan struct{}),
store: corroStore,
cluster: c,
localProxyServer: localProxyServer,
@@ -245,7 +258,9 @@ func NewMachine(config *Config) (*Machine, error) {
internalDNSIP := func() netip.Addr {
return m.IP()
}
m.docker = machinedocker.NewServer(dockerCli, db, internalDNSIP)
m.docker = machinedocker.NewServer(dockerCli, db, internalDNSIP,
machinedocker.WithNetworkReady(m.IsNetworkReady),
machinedocker.WithWaitForNetworkReady(m.WaitForNetworkReady))
m.localMachineServer = newGRPCServer(m, c, m.docker)
if m.Initialised() {
@@ -287,6 +302,9 @@ func (m *Machine) IP() netip.Addr {
}
func (m *Machine) Run(ctx context.Context) error {
// Create a cancellable context for the Run method to allow stopping the machine gracefully.
ctx, m.stop = context.WithCancel(ctx)
// Docker dependency is essential for the machine to function. Block until it's ready.
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
@@ -294,7 +312,7 @@ func (m *Machine) Run(ctx context.Context) error {
// Configure and start the corrosion service on the loopback if the machine is not initialised as a cluster
// member. This provides the store required for the machine to initialise a new cluster on it. Once the machine
// is initialised, the corrosion service is managed by the networkController.
// is initialised, the corrosion service is managed by the clusterController.
if !m.Initialised() {
if err := m.configureCorrosion(); err != nil {
return fmt.Errorf("configure corrosion service: %w", err)
@@ -304,6 +322,7 @@ func (m *Machine) Run(ctx context.Context) error {
if err := m.config.CorrosionService.Start(ctx); err != nil {
return fmt.Errorf("start corrosion service: %w", err)
}
slog.Info("Corrosion service started.")
}
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
@@ -314,147 +333,129 @@ func (m *Machine) Run(ctx context.Context) error {
if err != nil {
return fmt.Errorf("listen machine API unix socket %q: %w", m.config.MachineSockPath, err)
}
errGroup.Go(
func() error {
slog.Info("Starting local machine API server.", "path", m.config.MachineSockPath)
if err := m.localMachineServer.Serve(machineListener); err != nil {
return fmt.Errorf("local machine API server failed: %w", err)
}
return nil
},
)
errGroup.Go(func() error {
slog.Info("Starting local machine API server.", "path", m.config.MachineSockPath)
if err := m.localMachineServer.Serve(machineListener); err != nil {
return fmt.Errorf("local machine API server failed: %w", err)
}
return nil
})
// Start the local API proxy server.
proxyListener, err := listenUnixSocket(m.config.UncloudSockPath)
if err != nil {
return fmt.Errorf("listen API proxy unix socket %q: %w", m.config.UncloudSockPath, err)
}
errGroup.Go(
func() error {
slog.Info("Starting local API proxy server.", "path", m.config.UncloudSockPath)
if err := m.localProxyServer.Serve(proxyListener); err != nil {
return fmt.Errorf("local API proxy server failed: %w", err)
}
return nil
},
)
errGroup.Go(func() error {
slog.Info("Starting local API proxy server.", "path", m.config.UncloudSockPath)
if err := m.localProxyServer.Serve(proxyListener); err != nil {
return fmt.Errorf("local API proxy server failed: %w", err)
}
return nil
})
// Signal that the machine is ready.
close(m.started)
// Control loop for managing components that depend on the machine being initialised as a cluster member.
errGroup.Go(
func() error {
if !m.Initialised() {
slog.Info(
"Waiting for the machine to be initialised as a member of a cluster " +
"to start the network controller.",
)
// Wait for the machine to be initialised as a member of a cluster and run the cluster controller.
errGroup.Go(func() error {
if !m.Initialised() {
slog.Info(
"Waiting for the machine to be initialised as a member of a cluster to start the cluster controller.",
)
}
select {
case <-m.initialised:
m.cluster.UpdateMachineID(m.state.ID)
// Ensure the corrosion config is up to date, including a new gossip address if the machine
// has just joined a cluster.
if err := m.configureCorrosion(); err != nil {
return fmt.Errorf("configure corrosion service: %w", err)
}
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
slog.Info("Starting cluster controller.")
// Update the proxy director's local address to the machine's management IP address, allowing
// the proxy to identify which requests should be proxied to the local machine API server.
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
proxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnknownServiceHandler(
proxy.TransparentHandler(m.proxyDirector.Director),
),
)
// Create a new caddyconfig controller for managing the Caddy reverse proxy configuration.
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
caddyconfigCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
if err != nil {
return fmt.Errorf("create caddyconfig controller: %w", err)
}
var ctrl *networkController
// Error channel for communicating the termination of the network controller.
errCh := make(chan error)
for {
select {
// Wait for the machine to be initialised as a member of a cluster to start the network controller.
// It can be reset when leaving the cluster and then re-initialised again with a new configuration.
case <-m.initialised:
var err error
m.cluster.UpdateMachineID(m.state.ID)
// Ensure the corrosion config is up to date, including a new gossip address if the machine
// has just joined a cluster.
if err = m.configureCorrosion(); err != nil {
return fmt.Errorf("configure corrosion service: %w", err)
}
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
slog.Info("Starting network controller.")
// Update the proxy director's local address to the machine's management IP address, allowing
// the proxy to identify which requests should be proxied to the local machine API server.
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
proxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnknownServiceHandler(
proxy.TransparentHandler(m.proxyDirector.Director),
),
)
// Create a new Caddyfile controller for managing the Caddy reverse proxy configuration.
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
caddyfileCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
if err != nil {
return fmt.Errorf("create Caddyfile controller: %w", err)
}
dnsResolver := dns.NewClusterResolver(m.store)
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
if err != nil {
return fmt.Errorf("create embedded DNS server: %w", err)
}
ctrl, err = newNetworkController(
m.state,
m.store,
proxyServer,
m.config.CorrosionService,
m.config.DockerClient,
caddyfileCtrl,
dnsServer,
dnsResolver,
)
if err != nil {
return fmt.Errorf("initialise network controller: %w", err)
}
go func() {
if err = ctrl.Run(ctx); err != nil {
errCh <- fmt.Errorf("run network controller: %w", err)
} else {
slog.Info("Network controller stopped.")
errCh <- nil
}
}()
case err := <-errCh:
if err != nil {
return err
}
ctrl = nil
case <-ctx.Done():
// Wait for the network controller to stop before returning.
if ctrl != nil {
if err := <-errCh; err != nil {
return err
}
}
return nil
}
dnsResolver := dns.NewClusterResolver(m.store)
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
if err != nil {
return fmt.Errorf("create embedded DNS server: %w", err)
}
},
)
m.mu.Lock()
m.clusterCtrl, err = newClusterController(
m.state,
m.store,
proxyServer,
m.config.CorrosionService,
m.config.DockerClient,
m.networkReady,
caddyconfigCtrl,
dnsServer,
dnsResolver,
)
m.mu.Unlock()
if err != nil {
return fmt.Errorf("initialise cluster controller: %w", err)
}
if err = m.clusterCtrl.Run(ctx); err != nil {
return fmt.Errorf("run cluster controller: %w", err)
}
slog.Info("Cluster controller stopped.")
case <-ctx.Done():
// The context was cancelled before the machine was initialised.
}
return nil
})
// Shutdown goroutine.
errGroup.Go(
func() error {
<-ctx.Done()
slog.Info("Stopping local machine API server.")
// TODO: implement timeout for graceful shutdown.
m.localMachineServer.GracefulStop()
slog.Info("Local machine API server stopped.")
errGroup.Go(func() error {
var err error
slog.Info("Stopping local API proxy server.")
// TODO: implement timeout for graceful shutdown.
m.localProxyServer.GracefulStop()
// Close the proxy director to close all backend connections.
m.proxyDirector.Close()
slog.Info("Local API proxy server stopped.")
<-ctx.Done()
slog.Info("Stopping local machine API server.")
// TODO: implement timeout for graceful shutdown.
m.localMachineServer.GracefulStop()
slog.Info("Local machine API server stopped.")
m.config.DockerClient.Close()
return nil
},
)
slog.Info("Stopping local API proxy server.")
// TODO: implement timeout for graceful shutdown.
m.localProxyServer.GracefulStop()
// Close the proxy director to close all backend connections.
m.proxyDirector.Close()
slog.Info("Local API proxy server stopped.")
// Clean up the machine data and resources if the machine shutdown was initiated by a reset.
if m.resetting {
slog.Info("Cleaning up machine data and resources.")
if err = m.cleanup(); err != nil {
slog.Error("Failed to clean up machine data and resources.", "err", err)
}
}
m.config.DockerClient.Close()
return err
})
return errGroup.Wait()
}
@@ -483,7 +484,7 @@ func listenUnixSocket(path string) (net.Listener, error) {
// Ensure the parent directory exists and has the correct group permissions.
parent, _ := filepath.Split(path)
if err = os.MkdirAll(parent, 0750); err != nil {
if err = os.MkdirAll(parent, 0o750); err != nil {
return nil, fmt.Errorf("create directory %q: %w", parent, err)
}
if err = os.Chown(parent, -1, gid); err != nil {
@@ -537,13 +538,36 @@ func (m *Machine) configureCorrosion() error {
return fmt.Errorf("write corrosion config: %w", err)
}
if err := os.WriteFile(schemaPath, []byte(store.Schema), 0644); err != nil {
if err := os.WriteFile(schemaPath, []byte(store.Schema), 0o644); err != nil {
return fmt.Errorf("write corrosion schema: %w", err)
}
return nil
}
// cleanup removes the machine resources and persistent state.
func (m *Machine) cleanup() error {
var errs []error
m.mu.RLock()
clusterCtrl := m.clusterCtrl
m.mu.RUnlock()
if clusterCtrl != nil {
if err := clusterCtrl.Cleanup(); err != nil {
errs = append(errs, fmt.Errorf("cleanup cluster resources: %w", err))
}
}
if err := os.RemoveAll(m.config.DataDir); err != nil {
errs = append(errs,
fmt.Errorf("remove data directory with persistent machine state '%s': %w", m.config.DataDir, err))
} else {
slog.Info("Removed data directory storing persistent machine state.", "path", m.config.DataDir)
}
return errors.Join(errs...)
}
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
func (m *Machine) CheckPrerequisites(ctx context.Context, _ *emptypb.Empty) (*pb.CheckPrerequisitesResponse, error) {
// Check DNS port (UDP) availability.
@@ -776,6 +800,64 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
}, nil
}
// IsNetworkReady returns true if the Docker network is ready for containers.
func (m *Machine) IsNetworkReady() bool {
if !m.Initialised() {
// If machine is not initialized, there's no network to check
return false
}
// Check if network is ready by checking if the networkReady channel has been closed
select {
case <-m.networkReady:
return true
default:
return false
}
}
// WaitForNetworkReady waits for the Docker network to be ready for containers.
// It returns nil when the network is ready or an error if the context is cancelled.
func (m *Machine) WaitForNetworkReady(ctx context.Context) error {
if !m.Initialised() {
// If machine is not initialized, there's no network to wait for
return nil
}
// Wait for network to be ready or context to be cancelled
select {
case <-m.networkReady:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
func (m *Machine) Reset(ctx context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
if !m.Initialised() {
return nil, nil
}
// Check if the machine is already being reset to avoid concurrent resets.
m.mu.Lock()
if m.resetting {
m.mu.Unlock()
return nil, status.Error(codes.FailedPrecondition, "machine is already being reset")
}
m.resetting = true
m.mu.Unlock()
slog.Info("Resetting machine to a clean state.")
// Trigger the machine shutdown. The resetting boolean informs the machine to clean up its resources on shutdown.
// We can't clean up the resources synchronously here because this is an RPC call that depends on the running
// gRPC server and network.
m.stop()
return &emptypb.Empty{}, nil
}
// InspectService returns detailed information about a service and its containers stored in the cluster store.
func (m *Machine) InspectService(
ctx context.Context, req *pb.InspectServiceRequest,
+1 -1
View File
@@ -14,7 +14,7 @@ func MachineIP(subnet netip.Prefix) netip.Addr {
}
// ManagementIP returns the IPv6 address of a peer derived from the first 14 bytes of its public key.
// This address is intended for cluster management traffic.
// This address always starts with fdcc: and is intended for cluster management traffic.
func ManagementIP(publicKey secret.Secret) netip.Addr {
bytes := [16]byte{0xfd, 0xcc}
copy(bytes[2:], publicKey[:14])
+2 -1
View File
@@ -1,11 +1,12 @@
package network
import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"log/slog"
"net/netip"
"slices"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
+4 -3
View File
@@ -3,13 +3,14 @@ package tunnel
import (
"context"
"fmt"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
"net"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
)
const (
+2 -1
View File
@@ -2,10 +2,11 @@ package network
import (
"fmt"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
@@ -24,3 +24,7 @@ func (n *WireGuardNetwork) Run(ctx context.Context) error {
func (n *WireGuardNetwork) WatchEndpoints() <-chan EndpointChangeEvent {
return nil
}
func (n *WireGuardNetwork) Cleanup() error {
return errors.New("not implemented on darwin")
}
+53 -14
View File
@@ -6,18 +6,19 @@ import (
"context"
"errors"
"fmt"
"github.com/vishvananda/netlink"
"go4.org/netipx"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"log/slog"
"net"
"net/netip"
"slices"
"sync"
"time"
"github.com/psviderski/uncloud/internal/secret"
"github.com/vishvananda/netlink"
"go4.org/netipx"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type WireGuardNetwork struct {
@@ -26,6 +27,8 @@ type WireGuardNetwork struct {
peers map[string]*peer
// watchers is a list of channels that are notified when the endpoints of the peers change.
watchers []chan EndpointChangeEvent
// running indicates whether the network control loop (Run) is currently running.
running bool
// mu synchronises concurrent network configuration changes.
mu sync.Mutex
}
@@ -78,19 +81,15 @@ func (n *WireGuardNetwork) Configure(config Config) error {
}
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
machinePrefix := netip.PrefixFrom(MachineIP(config.Subnet), config.Subnet.Bits())
managementPrefix, err := addrToSingleIPPrefix(config.ManagementIP)
if err != nil {
return fmt.Errorf("parse management IP: %w", err)
}
addrs := []netip.Prefix{managementPrefix, machinePrefix}
addrs := []netip.Prefix{managementPrefix}
if err = n.updateAddresses(addrs); err != nil {
return err
}
slog.Info(
"Updated addresses of the WireGuard interface.",
"name", n.link.Attrs().Name, "addrs", addrs,
)
slog.Info("Updated addresses of the WireGuard interface.", "name", n.link.Attrs().Name, "addrs", addrs)
// Bring the WireGuard interface up if it's not already up.
if n.link.Attrs().Flags&unix.IFF_UP != unix.IFF_UP {
@@ -99,7 +98,7 @@ func (n *WireGuardNetwork) Configure(config Config) error {
}
slog.Info("Brought WireGuard interface up.", "name", n.link.Attrs().Name)
}
if err = n.updatePeerRoutes(); err != nil {
if err = n.updatePeerRoutes(MachineIP(config.Subnet)); err != nil {
return err
}
slog.Info(
@@ -129,7 +128,7 @@ func (n *WireGuardNetwork) configureDevice(config Config) error {
// rotations and connection disruptions.
n.peers = make(map[string]*peer, len(config.Peers))
wgPeers := make(map[string]*wgtypes.Peer, len(dev.Peers))
for i, _ := range dev.Peers {
for i := range dev.Peers {
wgPeers[secret.Secret(dev.Peers[i].PublicKey[:]).String()] = &dev.Peers[i]
}
for _, pc := range config.Peers {
@@ -202,7 +201,7 @@ func (n *WireGuardNetwork) updateAddresses(addrs []netip.Prefix) error {
// updatePeerRoutes adds routes to the peers via the WireGuard interface and removes old routes to peers
// that are no longer in the configuration.
func (n *WireGuardNetwork) updatePeerRoutes() error {
func (n *WireGuardNetwork) updatePeerRoutes(machineIP netip.Addr) error {
// Build a set of compacted IP ranges for all peers.
var ipsetBuilder netipx.IPSetBuilder
for _, p := range n.peers {
@@ -222,11 +221,18 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
// Add routes to the computed IP ranges via the WireGuard link.
for _, prefix := range ipset.Prefixes() {
dst := prefixToIPNet(prefix)
var src net.IP
// Use the machine IP as the source address for IPv4 routes to other peers.
if prefix.Addr().Is4() {
src = machineIP.AsSlice()
}
if err = netlink.RouteAdd(
&netlink.Route{
LinkIndex: n.link.Attrs().Index,
Scope: netlink.SCOPE_LINK,
Dst: &dst,
Src: src,
},
); err != nil {
if !errors.Is(err, unix.EEXIST) {
@@ -272,6 +278,14 @@ func (n *WireGuardNetwork) Run(ctx context.Context) error {
}
defer wg.Close()
n.mu.Lock()
if n.running {
n.mu.Unlock()
return errors.New("network is already running")
}
n.running = true
n.mu.Unlock()
ticker := time.NewTicker(1 * time.Second)
for {
select {
@@ -290,6 +304,11 @@ func (n *WireGuardNetwork) Run(ctx context.Context) error {
for _, ch := range n.watchers {
close(ch)
}
n.mu.Lock()
n.running = false
n.mu.Unlock()
return nil
}
}
@@ -427,3 +446,23 @@ func (n *WireGuardNetwork) notifyWatchers(ctx context.Context, events []Endpoint
}
return nil
}
// Cleanup deletes the WireGuard link. The network must not be running when this method is called.
func (n *WireGuardNetwork) Cleanup() error {
n.mu.Lock()
defer n.mu.Unlock()
if n.running {
return errors.New("network is still running, stop it before cleanup")
}
// Delete the WireGuard link.
name := n.link.Attrs().Name
if err := netlink.LinkDel(n.link); err != nil {
return fmt.Errorf("delete WireGuard link %q: %w", name, err)
}
n.link = nil
slog.Info("Deleted WireGuard interface.", "name", name)
return nil
}
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"sync"
"github.com/psviderski/uncloud/internal/machine/network"
)
@@ -71,7 +72,7 @@ func (c *State) Save() error {
return fmt.Errorf("state path not set")
}
dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0711); err != nil {
if err := os.MkdirAll(dir, 0o711); err != nil {
return fmt.Errorf("create state directory %q: %w", dir, err)
}
@@ -79,5 +80,5 @@ func (c *State) Save() error {
if err != nil {
return err
}
return os.WriteFile(c.path, data, 0600)
return os.WriteFile(c.path, data, 0o600)
}
+3 -2
View File
@@ -4,11 +4,12 @@ import (
"context"
"encoding/json"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/psviderski/uncloud/pkg/api"
"log/slog"
"strings"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/psviderski/uncloud/pkg/api"
)
const (
+90 -2
View File
@@ -5,17 +5,19 @@ import (
_ "embed"
"errors"
"fmt"
"google.golang.org/protobuf/encoding/protojson"
"log/slog"
"github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"google.golang.org/protobuf/encoding/protojson"
)
var (
//go:embed schema.sql
Schema string
ErrKeyNotFound = errors.New("key not found")
ErrKeyNotFound = errors.New("key not found")
ErrMachineNotFound = errors.New("machine not found")
)
// Store is a cluster store backed by a distributed Corrosion database.
@@ -66,6 +68,53 @@ func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
return nil
}
func (s *Store) GetMachine(ctx context.Context, machineID string) (*pb.MachineInfo, error) {
if machineID == "" {
return nil, fmt.Errorf("machine ID cannot be empty")
}
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines WHERE id = ?", machineID)
if err != nil {
return nil, fmt.Errorf("query machine: %w", err)
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("query error: %w", err)
}
return nil, fmt.Errorf("%w: %s", ErrMachineNotFound, machineID)
}
var mJSON string
if err = rows.Scan(&mJSON); err != nil {
return nil, fmt.Errorf("scan machine info: %w", err)
}
if mJSON == "" {
return nil, fmt.Errorf("machine info is empty for id %s", machineID)
}
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
var m pb.MachineInfo
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, fmt.Errorf("unmarshal machine info for id %s: %w", machineID, err)
}
// Validate the unmarshaled data. just in case
if m.Id != machineID {
return nil, fmt.Errorf("machine ID mismatch: expected %s, got %s", machineID, m.Id)
}
if m.Network != nil {
if err = m.Network.Validate(); err != nil {
return nil, fmt.Errorf("invalid network configuration for machine %s: %w", m.Id, err)
}
}
return &m, nil
}
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
if err != nil {
@@ -95,6 +144,45 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
return machines, nil
}
func (s *Store) UpdateMachine(ctx context.Context, m *pb.MachineInfo) error {
if m == nil {
return fmt.Errorf("machine info cannot be nil")
}
if m.Id == "" {
return fmt.Errorf("machine ID cannot be empty")
}
mJSON, err := protojson.Marshal(m)
if err != nil {
return fmt.Errorf("marshal machine info: %w", err)
}
result, err := s.corro.ExecContext(ctx, "UPDATE machines SET info = ? WHERE id = ?", string(mJSON), m.Id)
if err != nil {
return fmt.Errorf("update machine: %w", err)
}
// Check if machine exists
if result.RowsAffected == 0 {
return fmt.Errorf("%w: %s", ErrMachineNotFound, m.Id)
}
return nil
}
func (s *Store) DeleteMachine(ctx context.Context, id string) error {
result, err := s.corro.ExecContext(ctx, "DELETE FROM machines WHERE id = ?", id)
if err != nil {
return fmt.Errorf("delete machine: %w", err)
}
// Check if machine was deleted.
if result.RowsAffected == 0 {
return fmt.Errorf("%w: %s", ErrMachineNotFound, id)
}
return nil
}
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
// receive any values, it just signals when a machine has been added, updated, or deleted in the database.
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
+1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/netip"
"strings"
"github.com/psviderski/uncloud/internal/secret"
)
+2 -1
View File
@@ -3,9 +3,10 @@ package sshexec
import (
"context"
"fmt"
"golang.org/x/crypto/ssh"
"io"
"strings"
"golang.org/x/crypto/ssh"
)
type Remote struct {
+1
View File
@@ -2,6 +2,7 @@ package ucind
import (
"errors"
"github.com/docker/docker/client"
)
View File
+2 -1
View File
@@ -20,7 +20,7 @@ Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish
### Initialisation
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`. If you want to avoid using uncloud's managed DNS service, add the `--no-dns` flag.
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
@@ -37,6 +37,7 @@ Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
* To reserve a domain name, run `uc dns reserve`
* To release a domain name, run `uc dns release`.
* To see the domain name, run `uc dns show`
* To avoid using the Uncloud managed DNS service, use the `--no-dns` flag on your `uc machine init` command.
### Running a service
+2
View File
@@ -42,6 +42,8 @@ type ImageClient interface {
type MachineClient interface {
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
ListMachines(ctx context.Context, filter *MachineFilter) (MachineMembersList, error)
UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.MachineInfo, error)
RenameMachine(ctx context.Context, nameOrID, newName string) (*pb.MachineInfo, error)
}
type ServiceClient interface {
+3 -2
View File
@@ -1,10 +1,11 @@
package api
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPortSpec_Validate(t *testing.T) {
+2 -1
View File
@@ -1,10 +1,11 @@
package client
import (
"testing"
"github.com/distribution/reference"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestLatestCaddyImage(t *testing.T) {
+2
View File
@@ -19,6 +19,8 @@ type Client struct {
connector Connector
conn *grpc.ClientConn
// TODO: refactor to not embed MachineClient and instead expose only required methods.
// Methods such as Reset or Inspect are ambiguous in the context of a machine+cluster client.
pb.MachineClient
pb.ClusterClient
// Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
+11 -2
View File
@@ -25,11 +25,16 @@ type Deployment struct {
Client Client
Project *types.Project
SpecResolver *deploy.ServiceSpecResolver
Strategy deploy.Strategy
state *scheduler.ClusterState
plan *deploy.SequenceOperation
}
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
return NewDeploymentWithStrategy(ctx, cli, project, nil)
}
func NewDeploymentWithStrategy(ctx context.Context, cli Client, project *types.Project, strategy deploy.Strategy) (*Deployment, error) {
state, err := scheduler.InspectClusterState(ctx, cli)
if err != nil {
return nil, fmt.Errorf("inspect cluster state: %w", err)
@@ -39,16 +44,20 @@ func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*De
if err != nil && !errors.Is(err, api.ErrNotFound) {
return nil, fmt.Errorf("get cluster domain: %w", err)
}
resolver := &deploy.ServiceSpecResolver{
// If the domain is not found (not reserved), an empty domain is used for the resolver.
ClusterDomain: domain,
}
if strategy == nil {
strategy = &deploy.RollingStrategy{State: state}
}
return &Deployment{
Client: cli,
Project: project,
SpecResolver: resolver,
Strategy: strategy,
state: state,
}, nil
}
@@ -90,7 +99,7 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
for _, spec := range serviceSpecs {
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
// Pass the update cluster state with scheduled volumes to the deployment.
deployment := deploy.NewDeployment(d.Client, spec, &deploy.RollingStrategy{State: d.state})
deployment := deploy.NewDeployment(d.Client, spec, d.Strategy)
servicePlan, err := deployment.Plan(ctx)
if err != nil {
return plan, fmt.Errorf("create deployment plan for service '%s': %w", spec.Name, err)
+83
View File
@@ -0,0 +1,83 @@
package compose
import (
"fmt"
"strings"
)
const MachinesExtensionKey = "x-machines"
// MachinesSource represents the parsed x-machines extension data as slice of strings
type MachinesSource []string
// DecodeMapstructure implements custom decoding for multiple input types
func (m *MachinesSource) DecodeMapstructure(value interface{}) error {
switch v := value.(type) {
case *MachinesSource:
// Handle case where compose-go passes a pointer to an already created instance
*m = *v
return nil
case MachinesSource:
// Handle case where compose-go passes a direct instance
*m = v
return nil
case string:
// Support single string value or comma-separated values
// x-machines: my-machine or x-machines: "machine-1,machine-2"
machines, err := parseMachineNames(v)
if err != nil {
return err
}
*m = MachinesSource(machines)
return nil
case []string:
// Support string array: x-machines: ["machine-1", "machine-2"]
machines, err := validateMachineNames(v)
if err != nil {
return err
}
*m = MachinesSource(machines)
return nil
case []interface{}:
// Support interface array that may come from YAML parsing
machineNames := make([]string, 0, len(v))
for i, machine := range v {
str, ok := machine.(string)
if !ok {
return fmt.Errorf("x-machines[%d] is not a string, got %T", i, machine)
}
machineNames = append(machineNames, str)
}
machines, err := validateMachineNames(machineNames)
if err != nil {
return err
}
*m = MachinesSource(machines)
return nil
default:
return fmt.Errorf("x-machines must be a string or list of strings, got %T", value)
}
}
// parseMachineNames parses a single string that may contain comma-separated machine names
func parseMachineNames(machinesStr string) ([]string, error) {
// Split by comma and process each machine name, works for both single and multiple values
parts := strings.Split(machinesStr, ",")
machines := make([]string, 0, len(parts))
for _, part := range parts {
machines = append(machines, strings.TrimSpace(part))
}
return validateMachineNames(machines)
}
// validateMachineNames validates machine names to ensure they are not empty and contain valid characters.
func validateMachineNames(machines []string) ([]string, error) {
for i, machine := range machines {
machine = strings.TrimSpace(machine)
if machine == "" {
return nil, fmt.Errorf("x-machines[%d] cannot be empty", i)
}
machines[i] = machine
}
return machines, nil
}
+107 -8
View File
@@ -2,6 +2,10 @@ package compose
import (
"fmt"
"net/netip"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
)
@@ -10,20 +14,52 @@ const PortsExtensionKey = "x-ports"
type PortsSource []string
// TransformServicesPortsExtension transforms the ports extension of all services in the project by replacing a string
// representation of each port with a parsed PortSpec.
// transformServicesPortsExtension transforms both standard 'ports' and 'x-ports' to PortSpecs.
func transformServicesPortsExtension(project *types.Project) (*types.Project, error) {
return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
ports, ok := service.Extensions[PortsExtensionKey].(PortsSource)
if !ok {
// Check for mutual exclusivity
hasStandardPorts := len(service.Ports) > 0
hasXPorts := service.Extensions[PortsExtensionKey] != nil
if hasStandardPorts && hasXPorts {
return service, fmt.Errorf("service %q cannot specify both 'ports' and 'x-ports' directives, use only one",
name)
}
var (
specs []api.PortSpec
err error
)
if hasStandardPorts {
// Convert standard ports directly to api.PortSpec
specs, err = convertStandardPortsToPortSpecs(service.Ports)
if err != nil {
return service, fmt.Errorf("convert standard 'ports' for service '%s': %w", name, err)
}
} else if hasXPorts {
// Use existing x-ports string-based processing for backward compatibility
var portsSource PortsSource
var ok bool
portsSource, ok = service.Extensions[PortsExtensionKey].(PortsSource)
if !ok {
return service, nil
}
// Parse the port strings using existing logic
specs, err = transformPortsExtension(portsSource)
if err != nil {
return service, err
}
} else {
// No ports specified
return service, nil
}
specs, err := transformPortsExtension(ports)
if err != nil {
return service, err
// Ensure extensions map exists before setting the port specs
if service.Extensions == nil {
service.Extensions = make(types.Extensions)
}
service.Extensions[PortsExtensionKey] = specs
return service, nil
})
@@ -41,3 +77,66 @@ func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) {
return specs, nil
}
// convertServicePortConfigToPortSpec converts types.ServicePortConfig directly to api.PortSpec
func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortSpec, error) {
spec := api.PortSpec{
ContainerPort: uint16(port.Target),
Protocol: port.Protocol,
Mode: port.Mode,
}
// Compose parser sets the default protocol to "tcp" and mode to "ingress". We still explicitly set these values
// to avoid relying on implicit behavior and improve code robustness.
if spec.Protocol == "" {
spec.Protocol = api.ProtocolTCP
}
if spec.Mode == "" {
spec.Mode = api.PortModeIngress
}
// Set published port if specified
if port.Published != "" {
if strings.Contains(port.Published, "-") {
// 'a-b:x' format is not automatically expanded by the compose parser and our PortSpec does not support port
// ranges for now.
return spec, fmt.Errorf("port range '%s' for published port is not supported, use a single port",
port.Published)
}
publishedPort, err := strconv.ParseUint(port.Published, 10, 16)
if err != nil {
return spec, fmt.Errorf("invalid published port %q: %w", port.Published, err)
}
spec.PublishedPort = uint16(publishedPort)
}
// Set host IP if specified
if port.HostIP != "" {
hostIP, err := netip.ParseAddr(port.HostIP)
if err != nil {
return spec, fmt.Errorf("invalid host IP %q: %w", port.HostIP, err)
}
spec.HostIP = hostIP
}
// Validate the resulting spec
if err := spec.Validate(); err != nil {
return spec, fmt.Errorf("invalid port configuration: %w", err)
}
return spec, nil
}
// convertStandardPortsToPortSpecs converts []types.ServicePortConfig directly to api.PortSpecs.
func convertStandardPortsToPortSpecs(ports []types.ServicePortConfig) ([]api.PortSpec, error) {
specs := make([]api.PortSpec, 0, len(ports))
for _, port := range ports {
spec, err := convertServicePortConfigToPortSpec(port)
if err != nil {
return nil, err
}
specs = append(specs, spec)
}
return specs, nil
}
+416
View File
@@ -0,0 +1,416 @@
package compose
import (
"net/netip"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertStandardPortsToPortSpecs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ports []types.ServicePortConfig
expected []api.PortSpec
wantErr string
}{
{
name: "multiple ports",
ports: []types.ServicePortConfig{
{Target: 8080, Published: "80", Protocol: "tcp", Mode: "ingress"},
{Target: 8443, Published: "443", Protocol: "tcp", Mode: "host"},
{Target: 5353, Published: "53", Protocol: "udp"},
{Target: 2222, Published: "22"},
},
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"},
{ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "ingress"},
{ContainerPort: 2222, PublishedPort: 22, Protocol: "tcp", Mode: "ingress"},
},
},
{
name: "empty ports",
ports: []types.ServicePortConfig{},
expected: make([]api.PortSpec, 0),
},
{
name: "single port no published",
ports: []types.ServicePortConfig{
{Target: 8080},
},
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 0, Protocol: "tcp", Mode: "ingress"},
},
},
{
name: "IPv6 host IP",
ports: []types.ServicePortConfig{
{Target: 8080, Published: "80", Protocol: "tcp", HostIP: "::1", Mode: "host"},
},
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostIP: mustParseAddr("::1")},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := convertStandardPortsToPortSpecs(tt.ports)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
// mustParseAddr is a helper function for tests
func mustParseAddr(s string) netip.Addr {
addr, err := netip.ParseAddr(s)
if err != nil {
panic(err)
}
return addr
}
func TestConvertServicePortConfigToPortSpec(t *testing.T) {
t.Parallel()
tests := []struct {
name string
port types.ServicePortConfig
expected api.PortSpec
wantErr string
}{
{
name: "basic port",
port: types.ServicePortConfig{
Target: 8080,
Published: "80",
Protocol: "tcp",
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 80,
Protocol: "tcp",
Mode: "ingress",
},
},
{
name: "port with defaults",
port: types.ServicePortConfig{
Target: 8080,
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 0,
Protocol: "tcp",
Mode: "ingress",
},
},
{
name: "host mode with IP",
port: types.ServicePortConfig{
Target: 8080,
Published: "80",
Protocol: "tcp",
Mode: "host",
HostIP: "127.0.0.1",
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 80,
Protocol: "tcp",
Mode: "host",
HostIP: mustParseAddr("127.0.0.1"),
},
},
{
name: "UDP protocol",
port: types.ServicePortConfig{
Target: 5353,
Published: "53",
Protocol: "udp",
},
expected: api.PortSpec{
ContainerPort: 5353,
PublishedPort: 53,
Protocol: "udp",
Mode: "ingress",
},
},
{
name: "IPv6 host IP",
port: types.ServicePortConfig{
Target: 8080,
Published: "80",
Protocol: "tcp",
Mode: "host",
HostIP: "::1",
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 80,
Protocol: "tcp",
Mode: "host",
HostIP: mustParseAddr("::1"),
},
},
{
name: "HTTP protocol",
port: types.ServicePortConfig{
Target: 8080,
Published: "80",
Protocol: "http",
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 80,
Protocol: "http",
Mode: "ingress",
},
},
{
name: "HTTPS protocol",
port: types.ServicePortConfig{
Target: 8080,
Published: "443",
Protocol: "https",
},
expected: api.PortSpec{
ContainerPort: 8080,
PublishedPort: 443,
Protocol: "https",
Mode: "ingress",
},
},
// Error cases
{
name: "invalid published port",
port: types.ServicePortConfig{
Target: 8080,
Published: "invalid",
},
wantErr: "invalid published port",
},
{
name: "invalid host IP",
port: types.ServicePortConfig{
Target: 8080,
Published: "80",
HostIP: "invalid",
},
wantErr: "invalid host IP",
},
{
name: "missing container port",
port: types.ServicePortConfig{
Published: "80",
},
wantErr: "container port must be non-zero",
},
{
name: "missing container port",
port: types.ServicePortConfig{
Published: "8000-9000",
},
wantErr: "port range '8000-9000' for published port is not supported",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := convertServicePortConfigToPortSpec(tt.port)
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTransformServicesPortsExtension_MutualExclusivity(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
wantErr string
}{
{
name: "both ports and x-ports specified",
content: `
services:
web:
image: nginx
ports:
- "80:8080"
x-ports:
- "443:8443/https"
`,
wantErr: `service "web" cannot specify both 'ports' and 'x-ports' directives, use only one`,
},
{
name: "only ports specified",
content: `
services:
web:
image: nginx
ports:
- "80:8080"
`,
},
{
name: "only x-ports specified",
content: `
services:
web:
image: nginx
x-ports:
- "80:8080/tcp@host"
`,
},
{
name: "no ports specified",
content: `
services:
web:
image: nginx
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
project, err := loadProjectFromContent(t, tt.content)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
// Verify service exists
service, err := project.GetService("web")
require.NoError(t, err)
// Check that ports were processed correctly
if specs, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
// Should have specs if ports were specified
assert.NotEmpty(t, specs)
}
})
}
}
func TestTransformServicesPortsExtension_StandardPorts(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
expected []api.PortSpec
}{
{
name: "standard ports short syntax",
content: `
services:
web:
image: nginx
ports:
- "80:8080"
- "443:8443/tcp"
- "53:5353/udp"
`,
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "ingress"},
},
},
{
name: "standard ports long syntax",
content: `
services:
web:
image: nginx
ports:
- target: 8080
published: 80
protocol: tcp
mode: ingress
- target: 8443
published: 443
protocol: tcp
mode: host
`,
expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
project, err := loadProjectFromContent(t, tt.content)
require.NoError(t, err)
service, err := project.GetService("web")
require.NoError(t, err)
specs, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec)
require.True(t, ok, "Service should have port specs")
assert.ElementsMatch(t, tt.expected, specs)
})
}
}
func TestTransformServicesPortsExtension_XPorts(t *testing.T) {
t.Parallel()
content := `
services:
web:
image: nginx
x-ports:
- "80:8080/tcp"
- "443:8443/https"
- "9090:9090/tcp@host"
`
project, err := loadProjectFromContent(t, content)
require.NoError(t, err)
service, err := project.GetService("web")
require.NoError(t, err)
specs, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec)
require.True(t, ok, "Service should have port specs")
// Just verify that x-ports still work - don't check exact values as that's tested elsewhere
assert.Len(t, specs, 3)
}
+1
View File
@@ -25,6 +25,7 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project
// If none was selected, get default Compose file names from current or parent folders.
composecli.WithDefaultConfigPath,
composecli.WithExtension(PortsExtensionKey, PortsSource{}),
composecli.WithExtension(MachinesExtensionKey, MachinesSource{}),
}
options, err := composecli.NewProjectOptions(
+4 -1
View File
@@ -55,13 +55,16 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
},
Name: serviceName,
Mode: api.ServiceModeReplicated,
// TODO: implement and map x-machines to Placement.
}
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
spec.Ports = ports
}
if machines, ok := service.Extensions[MachinesExtensionKey].(MachinesSource); ok {
spec.Placement.Machines = []string(machines)
}
// Map LogDriver if specified
if service.Logging != nil && service.Logging.Driver != "" {
spec.Container.LogDriver = &api.LogDriver{
+187 -14
View File
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/mount"
"github.com/docker/go-units"
@@ -18,24 +18,39 @@ import (
"github.com/stretchr/testify/require"
)
// loadProjectFromFile loads a compose project from a YAML file
func loadProjectFromFile(t *testing.T, filename string) *types.Project {
// loadProjectFromContent loads a compose project from YAML content
func loadProjectFromContent(t *testing.T, content string) (*types.Project, error) {
t.Helper()
ctx := context.Background()
path := filepath.Join("testdata", filename)
options, err := cli.NewProjectOptions(
[]string{path},
cli.WithName(FakeProjectName),
cli.WithOsEnv,
cli.WithDotEnv,
)
require.NoError(t, err)
configDetails := types.ConfigDetails{
ConfigFiles: []types.ConfigFile{
{
Filename: "docker-compose.yml",
Content: []byte(content),
},
},
}
project, err := options.LoadProject(ctx)
require.NoError(t, err)
project, err := loader.LoadWithContext(ctx, configDetails, func(o *loader.Options) {
o.SetProjectName("test", true)
// Register our custom extensions
if o.KnownExtensions == nil {
o.KnownExtensions = map[string]any{}
}
o.KnownExtensions[PortsExtensionKey] = PortsSource{}
o.KnownExtensions[MachinesExtensionKey] = MachinesSource{}
})
if err != nil {
return nil, err
}
return project
// Apply ports extension transformation since we're not using LoadProject
if project, err = transformServicesPortsExtension(project); err != nil {
return nil, err
}
return project, nil
}
func TestServiceSpecFromCompose(t *testing.T) {
@@ -236,3 +251,161 @@ func TestServiceSpecFromCompose(t *testing.T) {
})
}
}
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
tests := []struct {
name string
composeYAML string
expected api.Placement
expectError bool
}{
{
name: "valid x-machines with string array",
composeYAML: `
services:
test:
image: nginx
x-machines: ["machine-1", "machine-2"]
`,
expected: api.Placement{
Machines: []string{"machine-1", "machine-2"},
},
},
{
name: "valid x-machines with single string",
composeYAML: `
services:
test:
image: nginx
x-machines: my-machine
`,
expected: api.Placement{
Machines: []string{"my-machine"},
},
},
{
name: "valid x-machines with single quoted string",
composeYAML: `
services:
test:
image: nginx
x-machines: "machine-1"
`,
expected: api.Placement{
Machines: []string{"machine-1"},
},
},
{
name: "valid x-machines with numeric string",
composeYAML: `
services:
test:
image: nginx
x-machines: "123"
`,
expected: api.Placement{
Machines: []string{"123"},
},
},
{
name: "valid x-machines with comma-separated string",
composeYAML: `
services:
test:
image: nginx
x-machines: "machine-1,machine-2"
`,
expected: api.Placement{
Machines: []string{"machine-1", "machine-2"},
},
},
{
name: "valid x-machines with comma-separated string and spaces",
composeYAML: `
services:
test:
image: nginx
x-machines: "machine-1, machine-2, machine-3"
`,
expected: api.Placement{
Machines: []string{"machine-1", "machine-2", "machine-3"},
},
},
{
name: "empty x-machines array",
composeYAML: `
services:
test:
image: nginx
x-machines: []
`,
expected: api.Placement{
Machines: []string{},
},
},
{
name: "no x-machines",
composeYAML: `
services:
test:
image: nginx
`,
expected: api.Placement{},
},
{
name: "empty machine name in x-machines",
composeYAML: `
services:
test:
image: nginx
x-machines: ["machine-1", "", "machine-2"]
`,
expectError: true,
},
{
name: "empty machine name in comma-separated x-machines",
composeYAML: `
services:
test:
image: nginx
x-machines: "machine-1,,machine-2"
`,
expectError: true,
},
{
name: "x-machines with whitespace trimming",
composeYAML: `
services:
test:
image: nginx
x-machines: [" machine-1 ", "machine-2"]
`,
expected: api.Placement{
Machines: []string{"machine-1", "machine-2"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
project, err := loadProjectFromContent(t, tt.composeYAML)
if tt.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
// Convert to ServiceSpec
spec, err := ServiceSpecFromCompose(project, "test")
require.NoError(t, err)
if len(tt.expected.Machines) == 0 && len(spec.Placement.Machines) == 0 {
// Both are empty, consider them equal
return
}
assert.Equal(t, tt.expected, spec.Placement)
})
}
}
+4 -3
View File
@@ -3,13 +3,14 @@ package connector
import (
"context"
"fmt"
"net"
"strings"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/sshexec"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net"
"strings"
)
type SSHConnectorConfig struct {
@@ -62,7 +63,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
conn, dErr := c.client.DialContext(ctx, "unix", addr)
if dErr != nil {
return nil, fmt.Errorf(
"connect to machine API socket '%s' through SSH tunnel (is the Uncloud daemon running "+
"connect to machine API socket '%s' through SSH tunnel (is uncloud.service running "+
"on the remote machine and does the SSH user '%s' have permissions to access the socket?):"+
" %w",
addr, c.client.User(), dErr,

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